From 4fdad2d8058df04d610e20ae1d5b42da1b81f12b Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Sun, 13 Oct 2024 15:36:54 +0100 Subject: [PATCH] feat: project setup - Set the project's placeholder name to indieauth-server. - Update the magefiles and CI workflow. - Add the version command to print the app's build information. - Add the serve command to run the HTTP server. --- .forgejo/workflows/workflow.yaml | 34 +++++++++++++--- .golangci.yaml | 8 ++++ cmd/indieauth-server/main.go | 23 +++++++++++ go.mod | 3 ++ internal/executors/command.go | 24 ++++++++++++ internal/executors/errors.go | 9 +++++ internal/executors/executors.go | 17 ++++++++ internal/executors/help.go | 1 + internal/executors/serve.go | 55 ++++++++++++++++++++++++++ internal/executors/version.go | 58 +++++++++++++++++++++++++++ internal/info/info.go | 12 ++++++ internal/router/router.go | 9 +++++ magefiles/go.mod | 5 +++ magefiles/go.sum | 2 + magefiles/mage.go | 67 +++++++++++++++++++++++++++----- main.go | 24 ------------ 16 files changed, 312 insertions(+), 39 deletions(-) create mode 100644 cmd/indieauth-server/main.go create mode 100644 go.mod create mode 100644 internal/executors/command.go create mode 100644 internal/executors/errors.go create mode 100644 internal/executors/executors.go create mode 100644 internal/executors/help.go create mode 100644 internal/executors/serve.go create mode 100644 internal/executors/version.go create mode 100644 internal/info/info.go create mode 100644 internal/router/router.go create mode 100644 magefiles/go.mod create mode 100644 magefiles/go.sum delete mode 100644 main.go diff --git a/.forgejo/workflows/workflow.yaml b/.forgejo/workflows/workflow.yaml index 13935dc..511e196 100644 --- a/.forgejo/workflows/workflow.yaml +++ b/.forgejo/workflows/workflow.yaml @@ -1,26 +1,48 @@ --- -name: Workflow +name: CI on: pull_request: + branches: + - main types: - opened - synchronize jobs: - test: + tests: + name: Tests if: ${{ ! github.event.pull_request.draft }} runs-on: docker - env: - GO_TEST_VERBOSE: "1" - GO_TEST_COVER: "1" steps: - name: Checkout Repository uses: https://code.forgejo.org/actions/checkout@v4 - - name: Test + - name: Run tests uses: https://codeflow.dananglin.me.uk/actions/mage-ci@main with: target: test env: PROJECT_TEST_VERBOSE: "1" PROJECT_TEST_COVER: "1" + - name: Run gosec + uses: https://codeflow.dananglin.me.uk/actions/mage-ci@main + with: + target: gosec + + style: + name: Style + if: ${{ ! github.event.pull_request.draft }} + runs-on: docker + steps: + - name: Checkout Repository + uses: https://code.forgejo.org/actions/checkout@v4 + - name: Check formatting + uses: https://codeflow.dananglin.me.uk/actions/mage-ci@main + with: + target: gofmt + env: + PROJECT_FAIL_ON_FORMATTING: 1 + - name: Run staticcheck + uses: https://codeflow.dananglin.me.uk/actions/mage-ci@main + with: + target: staticcheck diff --git a/.golangci.yaml b/.golangci.yaml index 059f5bc..5861113 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -13,6 +13,14 @@ output: sort-results: true linters-settings: + depguard: + rules: + main: + files: + - $all + allow: + - $gostd + - codeflow.dananglin.me.uk/apollo/indieauth-server lll: line-length: 140 diff --git a/cmd/indieauth-server/main.go b/cmd/indieauth-server/main.go new file mode 100644 index 0000000..44c6db2 --- /dev/null +++ b/cmd/indieauth-server/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "log/slog" + "os" + + "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/executors" +) + +func main() { + // Set up logging + loggingLevel := new(slog.LevelVar) + + slogOpts := slog.HandlerOptions{Level: loggingLevel} + + logger := slog.New(slog.NewTextHandler(os.Stdout, &slogOpts)) + slog.SetDefault(logger) + loggingLevel.Set(slog.LevelInfo) + + if err := executors.Execute(os.Args[1:]); err != nil { + slog.Error(err.Error()) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..df0e4ee --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module codeflow.dananglin.me.uk/apollo/indieauth-server + +go 1.23.2 diff --git a/internal/executors/command.go b/internal/executors/command.go new file mode 100644 index 0000000..5712068 --- /dev/null +++ b/internal/executors/command.go @@ -0,0 +1,24 @@ +package executors + +type command struct { + name string + args []string +} + +func newCommand(args []string) command { + if len(args) == 0 { + return command{ + name: "help", + args: make([]string, 0), + } + } + + if len(args) == 1 { + return command{ + name: args[0], + args: make([]string, 0), + } + } + + return command{name: args[0], args: args[1:]} +} diff --git a/internal/executors/errors.go b/internal/executors/errors.go new file mode 100644 index 0000000..54c4f78 --- /dev/null +++ b/internal/executors/errors.go @@ -0,0 +1,9 @@ +package executors + +type UnrecognisedCommandError struct { + command string +} + +func (e UnrecognisedCommandError) Error() string { + return "unrecognised command: " + e.command +} diff --git a/internal/executors/executors.go b/internal/executors/executors.go new file mode 100644 index 0000000..b83af61 --- /dev/null +++ b/internal/executors/executors.go @@ -0,0 +1,17 @@ +package executors + +func Execute(args []string) error { + command := newCommand(args) + + executorFuncMap := map[string]func(args []string) error{ + "serve": executeServeCommand, + "version": executeVersionCommand, + } + + executeFunc, ok := executorFuncMap[command.name] + if !ok { + return UnrecognisedCommandError{command.name} + } + + return executeFunc(command.args) +} diff --git a/internal/executors/help.go b/internal/executors/help.go new file mode 100644 index 0000000..240f649 --- /dev/null +++ b/internal/executors/help.go @@ -0,0 +1 @@ +package executors diff --git a/internal/executors/serve.go b/internal/executors/serve.go new file mode 100644 index 0000000..f4cac2e --- /dev/null +++ b/internal/executors/serve.go @@ -0,0 +1,55 @@ +package executors + +import ( + "flag" + "fmt" + "log/slog" + "net/http" + "time" + + "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/info" + "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/router" +) + +type serveExecutor struct { + *flag.FlagSet + + address string +} + +func executeServeCommand(args []string) error { + executorName := "serve" + + executor := serveExecutor{ + FlagSet: flag.NewFlagSet(executorName, flag.ExitOnError), + } + + executor.StringVar(&executor.address, "address", "0.0.0.0:8080", "The address that the server will listen on") + + if err := executor.Parse(args); err != nil { + return fmt.Errorf("(%s) flag parsing error: %w", executorName, err) + } + + if err := executor.execute(); err != nil { + return fmt.Errorf("(%s) execution error: %w", executorName, err) + } + + return nil +} + +func (e *serveExecutor) execute() error { + server := http.Server{ + Addr: e.address, + Handler: router.NewServeMux(), + ReadHeaderTimeout: 1 * time.Second, + } + + slog.Info(info.ApplicationName+" is listening for web requests", "address", e.address) + + err := server.ListenAndServe() + if err != nil { + return fmt.Errorf("error running the server: %w", err) + } + + return nil +} diff --git a/internal/executors/version.go b/internal/executors/version.go new file mode 100644 index 0000000..762e48a --- /dev/null +++ b/internal/executors/version.go @@ -0,0 +1,58 @@ +package executors + +import ( + "flag" + "fmt" + "os" + "strings" + "text/tabwriter" + + "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/info" +) + +type versionExecutor struct { + *flag.FlagSet + + showFullVersion bool +} + +func executeVersionCommand(args []string) error { + executorName := "version" + + executor := versionExecutor{ + FlagSet: flag.NewFlagSet(executorName, flag.ExitOnError), + } + + executor.BoolVar(&executor.showFullVersion, "full", false, "Print the applications full build information") + + if err := executor.Parse(args); err != nil { + return fmt.Errorf("(%s) flag parsing error: %w", executorName, err) + } + + executor.printVersion() + + return nil +} + +func (e *versionExecutor) printVersion() { + if !e.showFullVersion { + fmt.Fprintf(os.Stdout, "%s %s\n", info.ApplicationName, info.BinaryVersion) + + return + } + + var builder strings.Builder + + builder.WriteString(info.ApplicationName + "\n\n") + + tableWriter := tabwriter.NewWriter(&builder, 0, 4, 1, ' ', 0) + + _, _ = tableWriter.Write([]byte("Version:" + "\t" + info.BinaryVersion + "\n")) + _, _ = tableWriter.Write([]byte("Git commit:" + "\t" + info.GitCommit + "\n")) + _, _ = tableWriter.Write([]byte("Go version:" + "\t" + info.GoVersion + "\n")) + _, _ = tableWriter.Write([]byte("Build date:" + "\t" + info.BuildTime + "\n")) + + _ = tableWriter.Flush() + + _, _ = os.Stdout.WriteString(builder.String()) +} diff --git a/internal/info/info.go b/internal/info/info.go new file mode 100644 index 0000000..266537b --- /dev/null +++ b/internal/info/info.go @@ -0,0 +1,12 @@ +package info + +const ( + ApplicationName string = "indieauth-server" +) + +var ( + BinaryVersion string //nolint:gochecknoglobals + BuildTime string //nolint:gochecknoglobals + GoVersion string //nolint:gochecknoglobals + GitCommit string //nolint:gochecknoglobals +) diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..0c2cae6 --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,9 @@ +package router + +import "net/http" + +func NewServeMux() *http.ServeMux { + mux := http.NewServeMux() + + return mux +} diff --git a/magefiles/go.mod b/magefiles/go.mod new file mode 100644 index 0000000..dd5a3b1 --- /dev/null +++ b/magefiles/go.mod @@ -0,0 +1,5 @@ +module codeflow.dananglin.me.uk/apollo/indieauth-server/magefiles + +go 1.23.2 + +require github.com/magefile/mage v1.15.0 diff --git a/magefiles/go.sum b/magefiles/go.sum new file mode 100644 index 0000000..4ee1b87 --- /dev/null +++ b/magefiles/go.sum @@ -0,0 +1,2 @@ +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= diff --git a/magefiles/mage.go b/magefiles/mage.go index 1a12267..cdd1d29 100644 --- a/magefiles/mage.go +++ b/magefiles/mage.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "time" "github.com/magefile/mage/mg" @@ -14,13 +15,15 @@ import ( ) const ( - app = "binary" + app = "indieauth-server" defaultInstallPrefix = "/usr/local" - envInstallPrefix = "PROJECT_INSTALL_PREFIX" - envTestVerbose = "PROJECT_TEST_VERBOSE" - envTestCover = "PROJECT_TEST_COVER" - envBuildRebuildAll = "PROJECT_BUILD_REBUILD_ALL" - envBuildVerbose = "PROJECT_BUILD_VERBOSE" + + envInstallPrefix = "PROJECT_INSTALL_PREFIX" + envTestVerbose = "PROJECT_TEST_VERBOSE" + envTestCover = "PROJECT_TEST_COVER" + envBuildRebuildAll = "PROJECT_BUILD_REBUILD_ALL" + envBuildVerbose = "PROJECT_BUILD_VERBOSE" + envFailOnFormatting = "PROJECT_FAIL_ON_FORMATTING" ) var ( @@ -52,11 +55,46 @@ func Lint() error { return sh.RunV("golangci-lint", "run", "--color", "always") } +func Gosec() error { + return sh.RunV("gosec", "./...") +} + +func Staticcheck() error { + return sh.RunV("staticcheck", "./...") +} + +// Gofmt checks the code for formatting. +// To fail on formatting set PROJECT_FAIL_ON_FORMATTING=1 +func Gofmt() error { + output, err := sh.Output("go", "fmt", "./...") + if err != nil { + return err + } + + formattedFiles := "" + + for _, file := range strings.Split(output, "\n") { + formattedFiles += "\n- " + file + } + + if os.Getenv(envFailOnFormatting) != "1" { + fmt.Println(formattedFiles) + + return nil + } + + if len(output) != 0 { + return fmt.Errorf("The following files needed to be formatted: %s", formattedFiles) + } + + return nil +} + // Build build the executable. // To rebuild packages that are already up-to-date set PROJECT_BUILD_REBUILD_ALL=1 // To enable verbose mode set PROJECT_BUILD_VERBOSE=1 func Build() error { - main := "main.go" + main := "./cmd/" + app flags := ldflags() build := sh.RunCmd("go", "build") args := []string{"-ldflags=" + flags, "-o", binary} @@ -110,10 +148,21 @@ func Clean() error { // ldflags returns the build flags. func ldflags() string { - ldflagsfmt := "-s -w -X main.binaryVersion=%s -X main.gitCommit=%s -X main.goVersion=%s -X main.buildTime=%s" + versionPackage := "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/info" + binaryVersionVar := versionPackage + "." + "BinaryVersion" + gitCommitVar := versionPackage + "." + "GitCommit" + goVersionVar := versionPackage + "." + "GoVersion" + buildTimeVar := versionPackage + "." + "BuildTime" + ldflagsfmt := "-s -w -X %s=%s -X %s=%s -X %s=%s -X %s=%s" buildTime := time.Now().UTC().Format(time.RFC3339) - return fmt.Sprintf(ldflagsfmt, version(), gitCommit(), runtime.Version(), buildTime) + return fmt.Sprintf( + ldflagsfmt, + binaryVersionVar, version(), + gitCommitVar, gitCommit(), + goVersionVar, runtime.Version(), + buildTimeVar, buildTime, + ) } // version returns the latest git tag using git describe. diff --git a/main.go b/main.go deleted file mode 100644 index a3255e8..0000000 --- a/main.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "fmt" - "os" -) - -var ( - binaryVersion string - buildTime string - goVersion string - gitCommit string -) - -func main() { - if err := run(); err != nil { - fmt.Printf("ERROR: %v.\n", err) - os.Exit(1) - } -} - -func run() error { - return nil -} -- 2.45.2