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 }