github.com/mweagle/Sparta@v1.15.0/magefile.go (about)

     1  // +build mage
     2  
     3  // lint:file-ignore U1000 Ignore all  code, it's only for development
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"log"
    13  	"net/http"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"runtime"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/magefile/mage/mg" // mg contains helpful utility functions, like Deps
    22  	"github.com/magefile/mage/sh" // mg contains helpful utility functions, like Deps
    23  	"github.com/mholt/archiver"
    24  	spartamage "github.com/mweagle/Sparta/magefile"
    25  	"github.com/otiai10/copy"
    26  	"github.com/pkg/browser"
    27  	"github.com/pkg/errors"
    28  )
    29  
    30  const (
    31  	localWorkDir      = "./.sparta"
    32  	hugoVersion       = "0.69.2"
    33  	archIconsRootPath = "resources/describe/AWS-Architecture-Icons_PNG"
    34  	archIconsTreePath = "resources/describe/AWS-Architecture-Icons.tree.txt"
    35  )
    36  
    37  func xplatPath(pathParts ...string) string {
    38  	return filepath.Join(pathParts...)
    39  }
    40  
    41  var (
    42  	ignoreSubdirectoryPaths = []string{
    43  		xplatPath(".vendor"),
    44  		xplatPath(".sparta"),
    45  		xplatPath(".vscode"),
    46  		xplatPath("resources", "describe"),
    47  		xplatPath("docs_source", "themes"),
    48  	}
    49  	hugoDocsSourcePath = xplatPath(".", "docs_source")
    50  	hugoDocsPaths      = []string{
    51  		hugoDocsSourcePath,
    52  		xplatPath(".", "docs"),
    53  	}
    54  	hugoPath = filepath.Join(localWorkDir, "hugo")
    55  	header   = strings.Repeat("-", 80)
    56  )
    57  
    58  // Default target to run when none is specified
    59  // If not set, running mage will list available targets
    60  // var Default = Build
    61  
    62  func markdownSourceApply(commandParts ...string) error {
    63  	return spartamage.ApplyToSource("md", ignoreSubdirectoryPaths, commandParts...)
    64  }
    65  func goSourceApply(commandParts ...string) error {
    66  	return spartamage.ApplyToSource("go", ignoreSubdirectoryPaths, commandParts...)
    67  }
    68  
    69  func goFilteredSourceApply(ignorePatterns []string, commandParts ...string) error {
    70  	ignorePatterns = append(ignorePatterns, ignoreSubdirectoryPaths...)
    71  	return spartamage.ApplyToSource("go", ignorePatterns, commandParts...)
    72  }
    73  
    74  func gitCommit(shortVersion bool) (string, error) {
    75  	args := []string{
    76  		"rev-parse",
    77  	}
    78  	if shortVersion {
    79  		args = append(args, "--short")
    80  	}
    81  	args = append(args, "HEAD")
    82  	val, valErr := sh.Output("git", args...)
    83  	return strings.TrimSpace(val), valErr
    84  }
    85  
    86  // EnsureCleanTree ensures that the git tree is clean
    87  func EnsureCleanTree() error {
    88  	cleanTreeScript := [][]string{
    89  		// No dirty trees
    90  		{"git", "diff", "--exit-code"},
    91  	}
    92  	return spartamage.Script(cleanTreeScript)
    93  }
    94  
    95  ////////////////////////////////////////////////////////////////////////////////
    96  // START - DOCUMENTATION
    97  ////////////////////////////////////////////////////////////////////////////////
    98  
    99  // ensureWorkDir ensures that the scratch directory exists
   100  func ensureWorkDir() error {
   101  	return os.MkdirAll(localWorkDir, os.ModePerm)
   102  }
   103  
   104  func runHugoCommand(hugoCommandArgs ...string) error {
   105  	absHugoPath, absHugoPathErr := filepath.Abs(hugoPath)
   106  	if absHugoPathErr != nil {
   107  		return absHugoPathErr
   108  	}
   109  
   110  	// Get the git short value
   111  	gitSHA, gitSHAErr := gitCommit(true)
   112  	if gitSHAErr != nil {
   113  		return gitSHAErr
   114  	}
   115  
   116  	workDir, workDirErr := filepath.Abs(hugoDocsSourcePath)
   117  	if workDirErr != nil {
   118  		return workDirErr
   119  	}
   120  	var output io.Writer
   121  	if mg.Verbose() {
   122  		output = os.Stdout
   123  	}
   124  	cmd := exec.Command(absHugoPath, hugoCommandArgs...)
   125  	cmd.Env = append(os.Environ(), fmt.Sprintf("GIT_HEAD_COMMIT=%s", gitSHA))
   126  	cmd.Stderr = os.Stderr
   127  	cmd.Stdout = output
   128  	cmd.Dir = workDir
   129  	return cmd.Run()
   130  }
   131  
   132  func docsCopySourceTemplatesToDocs() error {
   133  	outputDir := filepath.Join(".",
   134  		"docs_source",
   135  		"static",
   136  		"source",
   137  		"resources",
   138  		"provision",
   139  		"apigateway")
   140  	rmErr := os.RemoveAll(outputDir)
   141  	if rmErr != nil {
   142  		return rmErr
   143  	}
   144  	// Create the directory
   145  	createErr := os.MkdirAll(outputDir, os.ModePerm)
   146  	if createErr != nil {
   147  		return createErr
   148  	}
   149  	inputDir := filepath.Join(".", "resources", "provision", "apigateway")
   150  	return copy.Copy(inputDir, outputDir)
   151  }
   152  
   153  // DocsInstallRequirements installs the required Hugo version
   154  func DocsInstallRequirements() error {
   155  	mg.SerialDeps(ensureWorkDir)
   156  
   157  	// Is hugo already installed?
   158  	spartamage.Log("Checking for Hugo version: %s", hugoVersion)
   159  
   160  	hugoOutput, hugoOutputErr := sh.Output(hugoPath, "version")
   161  	if hugoOutputErr == nil && strings.Contains(hugoOutput, hugoVersion) {
   162  		spartamage.Log("Hugo version %s already installed at %s", hugoVersion, hugoPath)
   163  		return nil
   164  	}
   165  
   166  	hugoArchiveName := ""
   167  	switch runtime.GOOS {
   168  	case "darwin":
   169  		hugoArchiveName = "macOS-64bit.tar.gz"
   170  	case "linux":
   171  		hugoArchiveName = "Linux-64bit.tar.gz"
   172  	default:
   173  		hugoArchiveName = fmt.Sprintf("UNSUPPORTED_%s", runtime.GOOS)
   174  	}
   175  
   176  	hugoURL := fmt.Sprintf("https://github.com/gohugoio/hugo/releases/download/v%s/hugo_extended_%s_%s",
   177  		hugoVersion,
   178  		hugoVersion,
   179  		hugoArchiveName)
   180  
   181  	spartamage.Log("Installing Hugo from source: %s", hugoURL)
   182  	outputArchive := filepath.Join(localWorkDir, "hugo.tar.gz")
   183  	outputFile, outputErr := os.Create(outputArchive)
   184  	if outputErr != nil {
   185  		return outputErr
   186  	}
   187  
   188  	hugoResp, hugoRespErr := http.Get(hugoURL)
   189  	if hugoRespErr != nil {
   190  		return hugoRespErr
   191  	}
   192  	defer hugoResp.Body.Close()
   193  
   194  	_, copyBytesErr := io.Copy(outputFile, hugoResp.Body)
   195  	if copyBytesErr != nil {
   196  		return copyBytesErr
   197  	}
   198  	// Great, go heads and untar it...
   199  	unarchiver := archiver.NewTarGz()
   200  	unarchiver.OverwriteExisting = true
   201  	untarErr := unarchiver.Unarchive(outputArchive, localWorkDir)
   202  	if untarErr != nil {
   203  		return untarErr
   204  	}
   205  	versionScript := [][]string{
   206  		{hugoPath, "version"},
   207  	}
   208  	return spartamage.Script(versionScript)
   209  }
   210  
   211  // DocsBuild builds the public documentation site in the /docs folder
   212  func DocsBuild() error {
   213  	cleanDocsDirectory := func() error {
   214  		docsDir, docsDirErr := filepath.Abs("docs")
   215  		if docsDirErr != nil {
   216  			return docsDirErr
   217  		}
   218  		spartamage.Log("Cleaning output directory: %s", docsDir)
   219  		return os.RemoveAll(docsDir)
   220  	}
   221  
   222  	mg.SerialDeps(DocsInstallRequirements,
   223  		cleanDocsDirectory,
   224  		docsCopySourceTemplatesToDocs)
   225  	return runHugoCommand()
   226  }
   227  
   228  // DocsCommit builds and commits the current
   229  // documentation with an autogenerated comment
   230  func DocsCommit() error {
   231  	mg.SerialDeps(DocsBuild)
   232  
   233  	commitNoMessageScript := make([][]string, 0)
   234  	for _, eachPath := range hugoDocsPaths {
   235  		commitNoMessageScript = append(commitNoMessageScript,
   236  			[]string{"git", "add", "--all", eachPath},
   237  		)
   238  	}
   239  	commitNoMessageScript = append(commitNoMessageScript,
   240  		[]string{"git", "commit", "-m", `"Documentation updates"`},
   241  	)
   242  	return spartamage.Script(commitNoMessageScript)
   243  }
   244  
   245  // DocsEdit starts a Hugo server and hot reloads the documentation at http://localhost:1313
   246  func DocsEdit() error {
   247  	mg.SerialDeps(DocsInstallRequirements,
   248  		docsCopySourceTemplatesToDocs)
   249  
   250  	editCommandArgs := []string{
   251  		"server",
   252  		"--disableFastRender",
   253  		"--watch",
   254  		"--forceSyncStatic",
   255  		"--verbose",
   256  	}
   257  	go func() {
   258  		spartamage.Log("Waiting for docs to build...")
   259  		time.Sleep(3 * time.Second)
   260  		browser.OpenURL("http://localhost:1313")
   261  	}()
   262  	return runHugoCommand(editCommandArgs...)
   263  }
   264  
   265  ////////////////////////////////////////////////////////////////////////////////
   266  // END - DOCUMENTATION
   267  ////////////////////////////////////////////////////////////////////////////////
   268  
   269  // GenerateAutomaticCode is the handler that runs the codegen part of things
   270  func GenerateAutomaticCode() error {
   271  	// First one is the embedded metric format
   272  	// https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
   273  	args := []string{"aws/cloudwatch/emf.schema.json",
   274  		"--capitalization",
   275  		"AWS",
   276  		"--capitalization",
   277  		"emf",
   278  		"--output",
   279  		"aws/cloudwatch/emf.go",
   280  		"--package",
   281  		"cloudwatch",
   282  	}
   283  	if mg.Verbose() {
   284  		args = append(args, "--verbose")
   285  	}
   286  	return sh.Run("gojsonschema", args...)
   287  }
   288  
   289  // GenerateBuildInfo creates the automatic buildinfo.go file so that we can
   290  // stamp the SHA into the binaries we build...
   291  func GenerateBuildInfo() error {
   292  	mg.SerialDeps(EnsureCleanTree)
   293  
   294  	// The first thing we need is the `git` SHA
   295  	gitSHA, gitSHAErr := gitCommit(false)
   296  	if gitSHAErr != nil {
   297  		return errors.Wrapf(gitSHAErr, "Failed to get git commit SHA")
   298  	}
   299  
   300  	// Super = update the buildinfo data
   301  	buildInfoTemplate := `package sparta
   302  
   303  // THIS FILE IS AUTOMATICALLY GENERATED
   304  // DO NOT EDIT
   305  // CREATED: %s
   306  
   307  // SpartaGitHash is the commit hash of this Sparta library
   308  const SpartaGitHash = "%s"
   309  `
   310  	updatedInfo := fmt.Sprintf(buildInfoTemplate, time.Now().UTC(), gitSHA)
   311  	// Write it to the output location...
   312  	writeErr := ioutil.WriteFile("./buildinfo.go", []byte(updatedInfo), os.ModePerm)
   313  
   314  	if writeErr != nil {
   315  		return writeErr
   316  	}
   317  	commitGenerateCommands := [][]string{
   318  		{"git", "diff"},
   319  		{"git", "commit", "-a", "-m", `"Autogenerated build info"`},
   320  	}
   321  	return spartamage.Script(commitGenerateCommands)
   322  
   323  }
   324  
   325  // GenerateConstants runs the set of commands that update the embedded CONSTANTS
   326  // for both local and AWS Lambda execution
   327  func GenerateConstants() error {
   328  	generateCommands := [][]string{
   329  		// Remove the tree output
   330  		{"rm",
   331  			"-fv",
   332  			xplatPath(archIconsTreePath),
   333  		},
   334  		//Create the embedded version
   335  		{"esc",
   336  			"-o",
   337  			"./CONSTANTS.go",
   338  			"-private",
   339  			"-pkg",
   340  			"sparta",
   341  			"./resources"},
   342  		//Create a secondary CONSTANTS_AWSBINARY.go file with empty content.
   343  		{"esc",
   344  			"-o",
   345  			"./CONSTANTS_AWSBINARY.go",
   346  			"-private",
   347  			"-pkg",
   348  			"sparta",
   349  			"./resources/awsbinary/README.md"},
   350  		//The next step will insert the
   351  		// build tags at the head of each file so that they are mutually exclusive
   352  		{"go",
   353  			"run",
   354  			"./cmd/insertTags/main.go",
   355  			"./CONSTANTS",
   356  			"!lambdabinary"},
   357  		{"go",
   358  			"run",
   359  			"./cmd/insertTags/main.go",
   360  			"./CONSTANTS_AWSBINARY",
   361  			"lambdabinary"},
   362  		// Create the tree output
   363  		{"tree",
   364  			"-Q",
   365  			"-o",
   366  			xplatPath(archIconsTreePath),
   367  			xplatPath(archIconsRootPath),
   368  		},
   369  		{"git",
   370  			"commit",
   371  			"-a",
   372  			"-m",
   373  			"Autogenerated constants"},
   374  	}
   375  	return spartamage.Script(generateCommands)
   376  }
   377  
   378  // EnsurePrealloc ensures that slices that could be preallocated are enforced
   379  func EnsurePrealloc() error {
   380  	// Super run some commands
   381  	preallocCommand := [][]string{
   382  		{"prealloc", "-set_exit_status", "./..."},
   383  	}
   384  	return spartamage.Script(preallocCommand)
   385  }
   386  
   387  // CIBuild is the task to build in the context of  CI pipeline
   388  func CIBuild() error {
   389  	mg.SerialDeps(EnsureCIBuildEnvironment,
   390  		Build,
   391  		Test)
   392  	return nil
   393  }
   394  
   395  // EnsureMarkdownSpelling ensures that all *.MD files are checked for common
   396  // spelling mistakes
   397  func EnsureMarkdownSpelling() error {
   398  	return markdownSourceApply("misspell", "-error")
   399  }
   400  
   401  // EnsureSpelling ensures that there are no misspellings in the source
   402  func EnsureSpelling() error {
   403  	ignoreFiles := []string{
   404  		"CONSTANTS*",
   405  	}
   406  	goSpelling := func() error {
   407  		return goFilteredSourceApply(ignoreFiles, "misspell", "-error")
   408  	}
   409  	mg.SerialDeps(
   410  		goSpelling,
   411  		EnsureMarkdownSpelling)
   412  	return nil
   413  }
   414  
   415  // EnsureVet ensures that the source has been `go vet`ted
   416  func EnsureVet() error {
   417  	verboseFlag := ""
   418  	if mg.Verbose() {
   419  		verboseFlag = "-v"
   420  	}
   421  	vetCommand := [][]string{
   422  		{"go", "vet", verboseFlag, "./..."},
   423  	}
   424  	return spartamage.Script(vetCommand)
   425  }
   426  
   427  // EnsureLint ensures that the source is `golint`ed
   428  func EnsureLint() error {
   429  	return goSourceApply("golint")
   430  }
   431  
   432  // EnsureGoFmt ensures that the source is `gofmt -s` is empty
   433  func EnsureGoFmt() error {
   434  
   435  	ignoreGlobs := append(ignoreSubdirectoryPaths,
   436  		"CONSTANTS.go",
   437  		"CONSTANTS_AWSBINARY.go")
   438  	return spartamage.ApplyToSource("go", ignoreGlobs, "gofmt", "-s", "-d")
   439  }
   440  
   441  // EnsureFormatted ensures that the source code is formatted with goimports
   442  func EnsureFormatted() error {
   443  	cmd := exec.Command("goimports", "-e", "-d", ".")
   444  	var stdout, stderr bytes.Buffer
   445  	cmd.Stdout = &stdout
   446  	cmd.Stderr = &stderr
   447  	err := cmd.Run()
   448  	if err != nil {
   449  		return err
   450  	}
   451  	if stdout.String() != "" {
   452  		if mg.Verbose() {
   453  			log.Print(stdout.String())
   454  		}
   455  		return errors.New("`goimports -e -d .` found import errors. Run `goimports -e -w .` to fix them")
   456  	}
   457  	return nil
   458  }
   459  
   460  // EnsureStaticChecks ensures that the source code passes static code checks
   461  func EnsureStaticChecks() error {
   462  	// https://staticcheck.io/
   463  	excludeChecks := "-exclude=G204,G505,G401,G601"
   464  	staticCheckErr := sh.Run("staticcheck",
   465  		"github.com/mweagle/Sparta/...")
   466  	if staticCheckErr != nil {
   467  		return staticCheckErr
   468  	}
   469  	// https://github.com/securego/gosec
   470  	if mg.Verbose() {
   471  		return sh.Run("gosec",
   472  			excludeChecks,
   473  			"./...")
   474  	}
   475  	return sh.Run("gosec",
   476  		excludeChecks,
   477  		"-quiet",
   478  		"./...")
   479  }
   480  
   481  // LogCodeMetrics ensures that the source code is formatted with goimports
   482  func LogCodeMetrics() error {
   483  	return sh.Run("gocloc", ".")
   484  }
   485  
   486  // EnsureAllPreconditions ensures that the source passes *ALL* static `ensure*`
   487  // precondition steps
   488  func EnsureAllPreconditions() error {
   489  	mg.SerialDeps(
   490  		EnsureVet,
   491  		EnsureLint,
   492  		EnsureGoFmt,
   493  		EnsureFormatted,
   494  		EnsureStaticChecks,
   495  		EnsureSpelling,
   496  		EnsurePrealloc,
   497  	)
   498  	return nil
   499  }
   500  
   501  // EnsureCIBuildEnvironment is the command that sets up the CI
   502  // environment to run the build.
   503  func EnsureCIBuildEnvironment() error {
   504  	// Super run some commands
   505  	ciCommands := [][]string{
   506  		{"go", "version"},
   507  	}
   508  	return spartamage.Script(ciCommands)
   509  }
   510  
   511  // Build the application
   512  func Build() error {
   513  	mg.Deps(EnsureAllPreconditions)
   514  	return sh.Run("go", "build", ".")
   515  }
   516  
   517  // Clean the working directory
   518  func Clean() error {
   519  	cleanCommands := [][]string{
   520  		{"go", "clean", "."},
   521  		{"rm", "-rf", "./graph.html"},
   522  		{"rsync", "-a", "--quiet", "--remove-source-files", "./vendor/", "$GOPATH/src"},
   523  	}
   524  	return spartamage.Script(cleanCommands)
   525  }
   526  
   527  // Describe runs the `TestDescribe` test to generate a describe HTML output
   528  // file at graph.html
   529  func Describe() error {
   530  	describeCommands := [][]string{
   531  		{"rm", "-rf", "./graph.html"},
   532  		{"go", "test", "-v", "-run", "TestDescribe"},
   533  	}
   534  	return spartamage.Script(describeCommands)
   535  }
   536  
   537  // Publish the latest source
   538  func Publish() error {
   539  	mg.SerialDeps(DocsBuild,
   540  		DocsCommit,
   541  		GenerateBuildInfo)
   542  
   543  	describeCommands := [][]string{
   544  		{"git", "push", "origin"},
   545  	}
   546  	return spartamage.Script(describeCommands)
   547  }
   548  
   549  // Test runs the Sparta tests
   550  func Test() error {
   551  	mg.SerialDeps(
   552  		EnsureAllPreconditions,
   553  	)
   554  	verboseFlag := ""
   555  	if mg.Verbose() {
   556  		verboseFlag = "-v"
   557  	}
   558  	testCommand := [][]string{
   559  		{"go", "test", verboseFlag, "-cover", "-race", "./..."},
   560  	}
   561  	return spartamage.Script(testCommand)
   562  }
   563  
   564  // TestCover runs the test and opens up the resulting report
   565  func TestCover() error {
   566  	mg.SerialDeps(
   567  		EnsureAllPreconditions,
   568  	)
   569  	coverageReport := fmt.Sprintf("%s/cover.out", localWorkDir)
   570  	testCoverCommands := [][]string{
   571  		{"go", "test", fmt.Sprintf("-coverprofile=%s", coverageReport), "."},
   572  		{"go", "tool", "cover", fmt.Sprintf("-html=%s", coverageReport)},
   573  		{"rm", coverageReport},
   574  		{"open", fmt.Sprintf("%s/cover.html", localWorkDir)},
   575  	}
   576  	return spartamage.Script(testCoverCommands)
   577  }
   578  
   579  // CompareAgainstMasterBranch is a convenience function to show the comparisons
   580  // of the current pushed branch against the master branch
   581  func CompareAgainstMasterBranch() error {
   582  	// Get the current branch, open a browser
   583  	// to the change...
   584  	// The first thing we need is the `git` branch
   585  	gitInfo, gitInfoErr := sh.Output("git", "rev-parse", "--abbrev-ref", "HEAD")
   586  	if gitInfoErr != nil {
   587  		return gitInfoErr
   588  	}
   589  	stdOutResult := strings.TrimSpace(gitInfo)
   590  	githubURL := fmt.Sprintf("https://github.com/mweagle/Sparta/compare/master...%s", stdOutResult)
   591  	return browser.OpenURL(githubURL)
   592  }