From 8312c006d1da3748422eb4fa156068c6c26d3182 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 31 Aug 2025 17:55:52 -0500 Subject: [PATCH 01/38] First pass at abstracting packages --- .github/workflows/main.yaml | 13 +-- Makefile | 21 +--- README.md | 2 + internal/ci/configfiles/embed.go | 10 -- internal/{git => ci}/git.go | 8 +- internal/ci/go/doc.go | 2 - internal/ci/markdown/doc.go | 2 - internal/ci/python/doc.go | 2 - internal/ci/run.go | 39 ++++---- internal/ci/shell/doc.go | 2 - internal/ci/util/doc.go | 2 - internal/git/doc.go | 2 - internal/hostinfo/doc.go | 2 + internal/{ci/util => hostinfo}/hostinfo.go | 12 +-- internal/tools/doc.go | 2 + internal/tools/go/doc.go | 2 + internal/{ci => tools}/go/tasks.go | 98 +++++++++---------- internal/{ci => tools}/go/versions.go | 14 +-- internal/tools/markdown/doc.go | 2 + internal/{ci => tools}/markdown/tasks.go | 20 ++-- internal/{ci => tools}/markdown/versions.go | 6 +- internal/tools/python/doc.go | 2 + internal/{ci => tools}/python/tasks.go | 50 +++++----- internal/{ci => tools}/python/versions.go | 12 +-- internal/tools/shell/doc.go | 2 + internal/{ci => tools}/shell/tasks.go | 32 +++--- internal/{ci => tools}/shell/versions.go | 10 +- .../toolcfg}/.markdownlint-cli2.yaml | 0 internal/tools/toolcfg/embed.go | 10 ++ .../toolcfg}/pyproject.toml | 0 .../configfiles => tools/toolcfg}/revive.toml | 0 .../toolcfg}/staticcheck.conf | 0 internal/{ci/util => tools}/types.go | 2 +- internal/{ci/util => tools}/util.go | 7 +- scripts/tag-release.sh | 67 +++++++++++++ scripts/xbuild.sh | 20 ++++ 36 files changed, 274 insertions(+), 203 deletions(-) delete mode 100644 internal/ci/configfiles/embed.go rename internal/{git => ci}/git.go (96%) delete mode 100644 internal/ci/go/doc.go delete mode 100644 internal/ci/markdown/doc.go delete mode 100644 internal/ci/python/doc.go delete mode 100644 internal/ci/shell/doc.go delete mode 100644 internal/ci/util/doc.go delete mode 100644 internal/git/doc.go create mode 100644 internal/hostinfo/doc.go rename internal/{ci/util => hostinfo}/hostinfo.go (79%) create mode 100644 internal/tools/doc.go create mode 100644 internal/tools/go/doc.go rename internal/{ci => tools}/go/tasks.go (62%) rename internal/{ci => tools}/go/versions.go (77%) create mode 100644 internal/tools/markdown/doc.go rename internal/{ci => tools}/markdown/tasks.go (59%) rename internal/{ci => tools}/markdown/versions.go (60%) create mode 100644 internal/tools/python/doc.go rename internal/{ci => tools}/python/tasks.go (63%) rename internal/{ci => tools}/python/versions.go (65%) create mode 100644 internal/tools/shell/doc.go rename internal/{ci => tools}/shell/tasks.go (64%) rename internal/{ci => tools}/shell/versions.go (66%) rename internal/{ci/configfiles => tools/toolcfg}/.markdownlint-cli2.yaml (100%) create mode 100644 internal/tools/toolcfg/embed.go rename internal/{ci/configfiles => tools/toolcfg}/pyproject.toml (100%) rename internal/{ci/configfiles => tools/toolcfg}/revive.toml (100%) rename internal/{ci/configfiles => tools/toolcfg}/staticcheck.conf (100%) rename internal/{ci/util => tools}/types.go (99%) rename internal/{ci/util => tools}/util.go (97%) create mode 100755 scripts/tag-release.sh create mode 100755 scripts/xbuild.sh diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 57b3e9a..43fa827 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,6 +1,3 @@ -# I mostly copied this straight from here: -# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images - name: main on: @@ -8,8 +5,10 @@ on: branches: - main pull_request: - branches: - - main + types: + - opened + - reopened + - synchronize jobs: ci: @@ -62,7 +61,9 @@ jobs: # context: . # file: Containerfile # push: true - # tags: ${{ steps.meta.outputs.tags }} + # tags: + # - "latest" + # - "${VERSION}" # # mainline # - name: Build and push mainline OCI image diff --git a/Makefile b/Makefile index c0994c4..cf46f6a 100644 --- a/Makefile +++ b/Makefile @@ -41,28 +41,9 @@ build: clean @printf 'built to %s\n' ./build/$(BINNAME) xbuild: clean - @for target in \ - darwin-amd64 \ - darwin-arm64 \ - linux-amd64 \ - linux-arm64 \ - ; \ - do \ - GOOS=$$(echo "$${target}" | cut -d'-' -f1) ; \ - GOARCH=$$(echo "$${target}" | cut -d'-' -f2) ; \ - outdir=build/"$${GOOS}-$${GOARCH}" ; \ - mkdir -p "$${outdir}" ; \ - printf "Building for %s-%s into build/ ...\n" "$${GOOS}" "$${GOARCH}" ; \ - GOOS="$${GOOS}" GOARCH="$${GOARCH}" $(RUN) go build -o "$${outdir}"/$(BINNAME) $(BINPATH) ; \ - done + @bash ./scripts/xbuild.sh package: xbuild - @mkdir -p dist - @cd build || exit 1; \ - for built in * ; do \ - printf 'Packaging for %s into dist/ ...\n' "$${built}" ; \ - cd $${built} && tar -czf ../../dist/$(BINNAME)_$${built}.tar.gz * && cd - >/dev/null ; \ - done clean: @rm -rf \ diff --git a/README.md b/README.md index ce1c629..ad8b42d 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details about developing `os ## Roadmap * Add `VERSION` check comparing to `main` as a CI task +* Add check for changelog Markdown file that matches what's in `VERSION` (we should also use that + file as the exact GH Release post contents) * Workstation setup * Have `oscar` manage Makefiles, dotfiles, etc. * CI additions diff --git a/internal/ci/configfiles/embed.go b/internal/ci/configfiles/embed.go deleted file mode 100644 index 025d5d3..0000000 --- a/internal/ci/configfiles/embed.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package ciconfig is used for storing embeddable config files for various CI tools, that are -// injected at runtime. -package ciconfig - -import "embed" - -// Files stores config files for each CI tool. -// -//go:embed *.conf *.toml *.yaml -var Files embed.FS diff --git a/internal/git/git.go b/internal/ci/git.go similarity index 96% rename from internal/git/git.go rename to internal/ci/git.go index 440023d..aaf9ec3 100644 --- a/internal/git/git.go +++ b/internal/ci/git.go @@ -1,4 +1,4 @@ -package igit +package ci import ( "fmt" @@ -25,8 +25,8 @@ type Status struct { UntrackedFiles []string } -// New returns a snapshot of Git information available at call-time. -func New() (*Git, error) { +// NewGit returns a snapshot of Git information available at call-time. +func NewGit() (*Git, error) { status, err := getRawStatus() if err != nil { return nil, err @@ -37,7 +37,7 @@ func New() (*Git, error) { }, nil } -// Update recalculates various Git metadata, respecting any existing baseline values set in [New]. +// Update recalculates various Git metadata, respecting any existing baseline values set in [NewGit]. func (g *Git) Update() error { status, err := getRawStatus() if err != nil { diff --git a/internal/ci/go/doc.go b/internal/ci/go/doc.go deleted file mode 100644 index 5552ce4..0000000 --- a/internal/ci/go/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package goci contains logic for running CI tasks for Go. -package goci diff --git a/internal/ci/markdown/doc.go b/internal/ci/markdown/doc.go deleted file mode 100644 index 85dae3c..0000000 --- a/internal/ci/markdown/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package markdownci contains logic for running CI tasks for Markdown. -package markdownci diff --git a/internal/ci/python/doc.go b/internal/ci/python/doc.go deleted file mode 100644 index 49db153..0000000 --- a/internal/ci/python/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package pythonci contains logic for running CI tasks for Python. -package pythonci diff --git a/internal/ci/run.go b/internal/ci/run.go index 5775a63..a0387ee 100644 --- a/internal/ci/run.go +++ b/internal/ci/run.go @@ -7,33 +7,32 @@ import ( "strings" "time" - goci "github.com/opensourcecorp/oscar/internal/ci/go" - markdownci "github.com/opensourcecorp/oscar/internal/ci/markdown" - pythonci "github.com/opensourcecorp/oscar/internal/ci/python" - shellci "github.com/opensourcecorp/oscar/internal/ci/shell" - ciutil "github.com/opensourcecorp/oscar/internal/ci/util" "github.com/opensourcecorp/oscar/internal/consts" - igit "github.com/opensourcecorp/oscar/internal/git" iprint "github.com/opensourcecorp/oscar/internal/print" + "github.com/opensourcecorp/oscar/internal/tools" + igo "github.com/opensourcecorp/oscar/internal/tools/go" + "github.com/opensourcecorp/oscar/internal/tools/markdown" + "github.com/opensourcecorp/oscar/internal/tools/python" + "github.com/opensourcecorp/oscar/internal/tools/shell" ) // TaskMap is a less-verbose type alias for mapping language names to function signatures that // return a language's tasks. -type TaskMap map[string][]ciutil.Tasker +type TaskMap map[string][]tools.Tasker // GetCITaskMap assembles the overall list of CI tasks, keyed by their language/tooling name func GetCITaskMap() (TaskMap, error) { - repo, err := ciutil.GetRepoComposition() + repo, err := tools.GetRepoComposition() if err != nil { return nil, fmt.Errorf("getting repo composition: %w", err) } out := make(TaskMap, 0) - for langName, getTasksFunc := range map[string]func(ciutil.Repo) []ciutil.Tasker{ - "Go": goci.Tasks, - "Python": pythonci.Tasks, - "Shell": shellci.Tasks, - "Markdown": markdownci.Tasks, + for langName, getTasksFunc := range map[string]func(tools.Repo) []tools.Tasker{ + "Go": igo.Tasks, + "Python": python.Tasks, + "Shell": shell.Tasks, + "Markdown": markdown.Tasks, } { tasks := getTasksFunc(repo) if len(tasks) > 0 { @@ -54,7 +53,7 @@ func Run() (err error) { runStartTime := time.Now() // Handle system init - if err := ciutil.InitSystem(); err != nil { + if err := tools.InitSystem(); err != nil { return fmt.Errorf("initializing system: %w", err) } // The mise config that oscar uses is written during init, so be sure to defer its removal here @@ -89,7 +88,7 @@ func Run() (err error) { iprint.Debugf("longestInfoTextLength: %d\n", longestInfoTextLength) // For tracking any changes to Git status etc. after each Task runs - git, err := igit.New() + git, err := NewGit() if err != nil { return fmt.Errorf("internal error: %w", err) } @@ -130,7 +129,7 @@ func Run() (err error) { } if runErr != nil || gitStatusHasChanged { - iprint.Errorf("FAILED! (%s)\n", ciutil.RunDurationString(taskStartTime)) + iprint.Errorf("FAILED! (%s)\n", tools.RunDurationString(taskStartTime)) iprint.Errorf("\n") if runErr != nil { @@ -146,19 +145,19 @@ func Run() (err error) { failures = append(failures, fmt.Sprintf("%s :: %s", lang, t.InfoText())) // Also need to reset the baseline status - git, err = igit.New() + git, err = NewGit() if err != nil { return fmt.Errorf("internal error: %w", err) } } else { - fmt.Printf("PASSED (%s)\n", ciutil.RunDurationString(taskStartTime)) + fmt.Printf("PASSED (%s)\n", tools.RunDurationString(taskStartTime)) } } } if len(failures) > 0 { iprint.Errorf("\n================================================================\n") - iprint.Errorf("The following checks failed and/or caused a git diff: (%s)\n", ciutil.RunDurationString(runStartTime)) + iprint.Errorf("The following checks failed and/or caused a git diff: (%s)\n", tools.RunDurationString(runStartTime)) for _, f := range failures { iprint.Errorf("- %s\n", f) } @@ -166,7 +165,7 @@ func Run() (err error) { return errors.New("one or more CI checks failed") } - fmt.Printf("All checks passed! (%s)\n", ciutil.RunDurationString(runStartTime)) + fmt.Printf("All checks passed! (%s)\n", tools.RunDurationString(runStartTime)) return err } diff --git a/internal/ci/shell/doc.go b/internal/ci/shell/doc.go deleted file mode 100644 index cf7eebe..0000000 --- a/internal/ci/shell/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package shellci contains logic for running CI tasks for Shell languages. -package shellci diff --git a/internal/ci/util/doc.go b/internal/ci/util/doc.go deleted file mode 100644 index 65a6afd..0000000 --- a/internal/ci/util/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package ciutil provides core types, helper functions, etc. for running CI tasks. -package ciutil diff --git a/internal/git/doc.go b/internal/git/doc.go deleted file mode 100644 index 8c6fcf5..0000000 --- a/internal/git/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package igit defines logic for interacting with Git on the host. -package igit diff --git a/internal/hostinfo/doc.go b/internal/hostinfo/doc.go new file mode 100644 index 0000000..df9de60 --- /dev/null +++ b/internal/hostinfo/doc.go @@ -0,0 +1,2 @@ +// Package hostinfo provides functionality for getting data about the host system. +package hostinfo diff --git a/internal/ci/util/hostinfo.go b/internal/hostinfo/hostinfo.go similarity index 79% rename from internal/ci/util/hostinfo.go rename to internal/hostinfo/hostinfo.go index 289ddf1..56c9d8d 100644 --- a/internal/ci/util/hostinfo.go +++ b/internal/hostinfo/hostinfo.go @@ -1,4 +1,4 @@ -package ciutil +package hostinfo import ( "fmt" @@ -6,7 +6,7 @@ import ( ) /* -HostInfoInput allows different tools to specify the different possible values for their host-related +Input allows different tools to specify the different possible values for their host-related information. This is primarily used when downloading a release artifact. This is necessary to provide because different tools use different values for their host info. For example: @@ -14,7 +14,7 @@ provide because different tools use different values for their host info. For ex - In addition to a kernel ID, some tools additionally specify an OS value (e.g. both "darwin" and "macos") */ -type HostInfoInput struct { +type Input struct { // The name a tool uses for Linux OSes (e.g. "linux", "unknown", etc.) OSLinux string // The name a tool uses for the Linux kernel (e.g. "linux", "linux-gnu", etc.) @@ -29,15 +29,15 @@ type HostInfoInput struct { ArchARM64 string } -// HostInfo holds the final host information values to be used in e.g. release downloads of a tool. +// HostInfo holds the determined host information values. type HostInfo struct { OS string Arch string Kernel string } -// GetHostInfo returns a populated [HostInfo], based on the provided [HostInfoInput] mappings. -func GetHostInfo(i HostInfoInput) (HostInfo, error) { +// Get returns a populated [HostInfo], based on the provided [Input] mappings. +func Get(i Input) (HostInfo, error) { var out HostInfo switch runtime.GOOS { diff --git a/internal/tools/doc.go b/internal/tools/doc.go new file mode 100644 index 0000000..a24f8dd --- /dev/null +++ b/internal/tools/doc.go @@ -0,0 +1,2 @@ +// Package tools contains common functionality for any tool usage. +package tools diff --git a/internal/tools/go/doc.go b/internal/tools/go/doc.go new file mode 100644 index 0000000..fa65bcd --- /dev/null +++ b/internal/tools/go/doc.go @@ -0,0 +1,2 @@ +// Package igo contains logic for running tasks for Go. +package igo diff --git a/internal/ci/go/tasks.go b/internal/tools/go/tasks.go similarity index 62% rename from internal/ci/go/tasks.go rename to internal/tools/go/tasks.go index 4044b0f..eaa564d 100644 --- a/internal/ci/go/tasks.go +++ b/internal/tools/go/tasks.go @@ -1,4 +1,4 @@ -package goci +package igo import ( "fmt" @@ -6,11 +6,11 @@ import ( "path/filepath" "slices" - ciconfig "github.com/opensourcecorp/oscar/internal/ci/configfiles" - ciutil "github.com/opensourcecorp/oscar/internal/ci/util" + "github.com/opensourcecorp/oscar/internal/tools" + "github.com/opensourcecorp/oscar/internal/tools/toolcfg" ) -// A list of tasks that all implement [ciutil.Tasker], for Go. +// A list of tasks that all implement [tools.Tasker], for Go. type ( goModCheckTask struct{} goFormatTask struct{} @@ -25,7 +25,7 @@ type ( goTestTask struct{} ) -var tasks = []ciutil.Tasker{ +var tasks = []tools.Tasker{ goModCheckTask{}, goFormatTask{}, generateCodeTask{}, @@ -40,7 +40,7 @@ var tasks = []ciutil.Tasker{ } // Tasks returns the list of CI tasks. -func Tasks(repo ciutil.Repo) []ciutil.Tasker { +func Tasks(repo tools.Repo) []tools.Tasker { if repo.HasGo { return tasks } @@ -48,87 +48,87 @@ func Tasks(repo ciutil.Repo) []ciutil.Tasker { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t goModCheckTask) InfoText() string { return "go.mod tidy check" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t goModCheckTask) Run() error { - if err := ciutil.RunCommand([]string{"go", "mod", "tidy"}); err != nil { + if err := tools.RunCommand([]string{"go", "mod", "tidy"}); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t goModCheckTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t goFormatTask) InfoText() string { return "Format" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t goFormatTask) Run() error { - if err := ciutil.RunCommand([]string{"go", "fmt", "./..."}); err != nil { + if err := tools.RunCommand([]string{"go", "fmt", "./..."}); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t goFormatTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t generateCodeTask) InfoText() string { return "Generate code" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t generateCodeTask) Run() error { - if err := ciutil.RunCommand([]string{"go", "generate", "./..."}); err != nil { + if err := tools.RunCommand([]string{"go", "generate", "./..."}); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t generateCodeTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t goBuildTask) InfoText() string { return "Build" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t goBuildTask) Run() error { - if err := ciutil.RunCommand([]string{"go", "build", "./..."}); err != nil { + if err := tools.RunCommand([]string{"go", "build", "./..."}); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t goBuildTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t goVetTask) InfoText() string { return "Vet" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t goVetTask) Run() error { - if err := ciutil.RunCommand([]string{"go", "vet", "./..."}); err != nil { + if err := tools.RunCommand([]string{"go", "vet", "./..."}); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t goVetTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t staticcheckTask) InfoText() string { return "Lint (staticcheck)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t staticcheckTask) Run() (err error) { - cfgFileContents, err := ciconfig.Files.ReadFile(filepath.Base(staticcheck.ConfigFilePath)) + cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(staticcheck.ConfigFilePath)) if err != nil { return fmt.Errorf("reading embedded file contents: %w", err) } @@ -144,7 +144,7 @@ func (t staticcheckTask) Run() (err error) { return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t staticcheckTask) Post() error { if err := os.RemoveAll(staticcheck.ConfigFilePath); err != nil { return fmt.Errorf("removing config file: %w", err) @@ -153,12 +153,12 @@ func (t staticcheckTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t reviveTask) InfoText() string { return "Lint (revive)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t reviveTask) Run() error { - cfgFileContents, err := ciconfig.Files.ReadFile(filepath.Base(revive.ConfigFilePath)) + cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(revive.ConfigFilePath)) if err != nil { return fmt.Errorf("reading embedded file contents: %w", err) } @@ -180,7 +180,7 @@ func (t reviveTask) Run() error { return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t reviveTask) Post() error { if err := os.RemoveAll(revive.ConfigFilePath); err != nil { return fmt.Errorf("removing config file: %w", err) @@ -189,10 +189,10 @@ func (t reviveTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t errcheckTask) InfoText() string { return "Lint (errcheck)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t errcheckTask) Run() error { if err := goRun(errcheck, "./..."); err != nil { return err @@ -201,13 +201,13 @@ func (t errcheckTask) Run() error { return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t errcheckTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t goImportsTask) InfoText() string { return "Format imports" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t goImportsTask) Run() error { args := []string{"-l", "-w", "."} if err := goRun(goimports, args...); err != nil { @@ -217,13 +217,13 @@ func (t goImportsTask) Run() error { return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t goImportsTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t govulncheckTask) InfoText() string { return "Vulnerability scan (govulncheck)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t govulncheckTask) Run() error { if err := goRun(govulncheck, "./..."); err != nil { return err @@ -232,31 +232,31 @@ func (t govulncheckTask) Run() error { return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t govulncheckTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t goTestTask) InfoText() string { return "Test" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t goTestTask) Run() error { - if err := ciutil.RunCommand([]string{"go", "test", "./..."}); err != nil { + if err := tools.RunCommand([]string{"go", "test", "./..."}); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t goTestTask) Post() error { return nil } // goRun is a wrapper for "go run" -func goRun(t ciutil.Tool, trailingArgs ...string) error { +func goRun(t tools.Tool, trailingArgs ...string) error { args := slices.Concat( []string{"go", "run", fmt.Sprintf("%s@%s", t.RemotePath, t.Version)}, trailingArgs, ) - if err := ciutil.RunCommand(args); err != nil { + if err := tools.RunCommand(args); err != nil { return fmt.Errorf("running 'go run': %w", err) } diff --git a/internal/ci/go/versions.go b/internal/tools/go/versions.go similarity index 77% rename from internal/ci/go/versions.go rename to internal/tools/go/versions.go index 9890823..c01ec3e 100644 --- a/internal/ci/go/versions.go +++ b/internal/tools/go/versions.go @@ -1,36 +1,36 @@ -package goci +package igo import ( "os" "path/filepath" - ciutil "github.com/opensourcecorp/oscar/internal/ci/util" + "github.com/opensourcecorp/oscar/internal/tools" ) var ( - staticcheck = ciutil.Tool{ + staticcheck = tools.Tool{ Name: "staticcheck", RemotePath: "honnef.co/go/tools/cmd/staticcheck", Version: "2025.1.1", ConfigFilePath: filepath.Join("./staticcheck.conf"), } - revive = ciutil.Tool{ + revive = tools.Tool{ Name: "revive", RemotePath: "github.com/mgechev/revive", Version: "v1.11.0", ConfigFilePath: filepath.Join(os.TempDir(), "revive.toml"), } - errcheck = ciutil.Tool{ + errcheck = tools.Tool{ Name: "errcheck", RemotePath: "github.com/kisielk/errcheck", Version: "v1.9.0", } - goimports = ciutil.Tool{ + goimports = tools.Tool{ Name: "goimports", RemotePath: "golang.org/x/tools/cmd/goimports", Version: "v0.35.0", } - govulncheck = ciutil.Tool{ + govulncheck = tools.Tool{ Name: "govulncheck", RemotePath: "golang.org/x/vuln/cmd/govulncheck", Version: "v1.1.4", diff --git a/internal/tools/markdown/doc.go b/internal/tools/markdown/doc.go new file mode 100644 index 0000000..3a6c622 --- /dev/null +++ b/internal/tools/markdown/doc.go @@ -0,0 +1,2 @@ +// Package markdown contains logic for running tasks for Markdown. +package markdown diff --git a/internal/ci/markdown/tasks.go b/internal/tools/markdown/tasks.go similarity index 59% rename from internal/ci/markdown/tasks.go rename to internal/tools/markdown/tasks.go index 6a2c3e0..4bb8ed4 100644 --- a/internal/ci/markdown/tasks.go +++ b/internal/tools/markdown/tasks.go @@ -1,24 +1,24 @@ -package markdownci +package markdown import ( "fmt" "os" "path/filepath" - ciconfig "github.com/opensourcecorp/oscar/internal/ci/configfiles" - ciutil "github.com/opensourcecorp/oscar/internal/ci/util" + "github.com/opensourcecorp/oscar/internal/tools" + "github.com/opensourcecorp/oscar/internal/tools/toolcfg" ) type ( markdownlintTask struct{} ) -var tasks = []ciutil.Tasker{ +var tasks = []tools.Tasker{ markdownlintTask{}, } // Tasks returns the list of CI tasks. -func Tasks(repo ciutil.Repo) []ciutil.Tasker { +func Tasks(repo tools.Repo) []tools.Tasker { if repo.HasMarkdown { return tasks } @@ -26,12 +26,12 @@ func Tasks(repo ciutil.Repo) []ciutil.Tasker { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t markdownlintTask) InfoText() string { return "Lint (markdownlint)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t markdownlintTask) Run() error { - cfgFileContents, err := ciconfig.Files.ReadFile(filepath.Base(markdownlint.ConfigFilePath)) + cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(markdownlint.ConfigFilePath)) if err != nil { return fmt.Errorf("reading embedded file contents: %w", err) } @@ -46,12 +46,12 @@ func (t markdownlintTask) Run() error { "**/*.md", } - if err := ciutil.RunCommand(args); err != nil { + if err := tools.RunCommand(args); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t markdownlintTask) Post() error { return nil } diff --git a/internal/ci/markdown/versions.go b/internal/tools/markdown/versions.go similarity index 60% rename from internal/ci/markdown/versions.go rename to internal/tools/markdown/versions.go index 1bf06d2..f4de820 100644 --- a/internal/ci/markdown/versions.go +++ b/internal/tools/markdown/versions.go @@ -1,14 +1,14 @@ -package markdownci +package markdown import ( "os" "path/filepath" - ciutil "github.com/opensourcecorp/oscar/internal/ci/util" + "github.com/opensourcecorp/oscar/internal/tools" ) var ( - markdownlint = ciutil.Tool{ + markdownlint = tools.Tool{ Name: "markdownlint-cli2", ConfigFilePath: filepath.Join(os.TempDir(), ".markdownlint-cli2.yaml"), } diff --git a/internal/tools/python/doc.go b/internal/tools/python/doc.go new file mode 100644 index 0000000..2b41654 --- /dev/null +++ b/internal/tools/python/doc.go @@ -0,0 +1,2 @@ +// Package python contains logic for running tasks for Python. +package python diff --git a/internal/ci/python/tasks.go b/internal/tools/python/tasks.go similarity index 63% rename from internal/ci/python/tasks.go rename to internal/tools/python/tasks.go index 080ff5f..27557ca 100644 --- a/internal/ci/python/tasks.go +++ b/internal/tools/python/tasks.go @@ -1,10 +1,10 @@ -package pythonci +package python import ( "fmt" "slices" - ciutil "github.com/opensourcecorp/oscar/internal/ci/util" + "github.com/opensourcecorp/oscar/internal/tools" ) type ( @@ -16,7 +16,7 @@ type ( mypyTask struct{} ) -var tasks = []ciutil.Tasker{ +var tasks = []tools.Tasker{ baseConfigTask{}, buildTask{}, ruffLintTask{}, @@ -26,7 +26,7 @@ var tasks = []ciutil.Tasker{ } // Tasks returns the list of CI tasks. -func Tasks(repo ciutil.Repo) []ciutil.Tasker { +func Tasks(repo tools.Repo) []tools.Tasker { if repo.HasPython { return tasks } @@ -34,38 +34,38 @@ func Tasks(repo ciutil.Repo) []ciutil.Tasker { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t baseConfigTask) InfoText() string { return "" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t baseConfigTask) Run() error { // ciutil.PlaceConfigFile("pyproject.toml") return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t baseConfigTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t buildTask) InfoText() string { return "Build" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t buildTask) Run() error { - if err := ciutil.RunCommand([]string{"uv", "build"}); err != nil { + if err := tools.RunCommand([]string{"uv", "build"}); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t buildTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t ruffLintTask) InfoText() string { return "Lint (ruff)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t ruffLintTask) Run() error { if err := pyRun(ruffLint, "check", "--fix", "./src"); err != nil { return err @@ -74,13 +74,13 @@ func (t ruffLintTask) Run() error { return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t ruffLintTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t ruffFormatTask) InfoText() string { return "Format (ruff)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t ruffFormatTask) Run() error { if err := pyRun(ruffFormat, "format", "./src"); err != nil { return err @@ -88,13 +88,13 @@ func (t ruffFormatTask) Run() error { return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t ruffFormatTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t pydoclintTask) InfoText() string { return "Lint (pydoclint)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t pydoclintTask) Run() error { if err := pyRun(pydoclint, "./src"); err != nil { return err @@ -103,13 +103,13 @@ func (t pydoclintTask) Run() error { return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t pydoclintTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t mypyTask) InfoText() string { return "Type-check (mypy)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t mypyTask) Run() error { if err := pyRun(mypy, "./src"); err != nil { return err @@ -118,16 +118,16 @@ func (t mypyTask) Run() error { return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t mypyTask) Post() error { return nil } // pyRun is a wrapper for "uvx" -func pyRun(t ciutil.Tool, trailingArgs ...string) error { +func pyRun(t tools.Tool, trailingArgs ...string) error { args := slices.Concat( []string{"uvx", fmt.Sprintf("%s@%s", t.Name, t.Version)}, trailingArgs, ) - if err := ciutil.RunCommand(args); err != nil { + if err := tools.RunCommand(args); err != nil { return fmt.Errorf("running 'uvx': %w", err) } diff --git a/internal/ci/python/versions.go b/internal/tools/python/versions.go similarity index 65% rename from internal/ci/python/versions.go rename to internal/tools/python/versions.go index 2a94e0a..a668c0f 100644 --- a/internal/ci/python/versions.go +++ b/internal/tools/python/versions.go @@ -1,25 +1,25 @@ -package pythonci +package python import ( - ciutil "github.com/opensourcecorp/oscar/internal/ci/util" + "github.com/opensourcecorp/oscar/internal/tools" ) var ( // NOTE: even though ruff is used for both linting & formatting, their implementations differ, // so we need two distinct [ciutil.Tool]s. - ruffLint = ciutil.Tool{ + ruffLint = tools.Tool{ Name: "ruff", Version: "0.12.7", } - ruffFormat = ciutil.Tool{ + ruffFormat = tools.Tool{ Name: "ruff", Version: "0.12.7", } - pydoclint = ciutil.Tool{ + pydoclint = tools.Tool{ Name: "pydoclint", Version: "0.6.6", } - mypy = ciutil.Tool{ + mypy = tools.Tool{ Name: "mypy", Version: "1.17.1", } diff --git a/internal/tools/shell/doc.go b/internal/tools/shell/doc.go new file mode 100644 index 0000000..fc6a5a9 --- /dev/null +++ b/internal/tools/shell/doc.go @@ -0,0 +1,2 @@ +// Package shell contains logic for running tasks for Shell languages. +package shell diff --git a/internal/ci/shell/tasks.go b/internal/tools/shell/tasks.go similarity index 64% rename from internal/ci/shell/tasks.go rename to internal/tools/shell/tasks.go index b214739..f151a8f 100644 --- a/internal/ci/shell/tasks.go +++ b/internal/tools/shell/tasks.go @@ -1,9 +1,9 @@ -package shellci +package shell import ( "fmt" - ciutil "github.com/opensourcecorp/oscar/internal/ci/util" + "github.com/opensourcecorp/oscar/internal/tools" ) type ( @@ -12,14 +12,14 @@ type ( batsTask struct{} ) -var tasks = []ciutil.Tasker{ +var tasks = []tools.Tasker{ shellcheckTask{}, shfmtTask{}, batsTask{}, } // Tasks returns the list of CI tasks. -func Tasks(repo ciutil.Repo) []ciutil.Tasker { +func Tasks(repo tools.Repo) []tools.Tasker { if repo.HasShell { return tasks } @@ -27,50 +27,50 @@ func Tasks(repo ciutil.Repo) []ciutil.Tasker { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t shellcheckTask) InfoText() string { return "Lint (shellcheck)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t shellcheckTask) Run() error { args := []string{"bash", "-c", fmt.Sprintf(` shopt -s globstar ls **/*.sh || exit 0 %s **/*.sh `, shellcheck.Name)} - if err := ciutil.RunCommand(args); err != nil { + if err := tools.RunCommand(args); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t shellcheckTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t shfmtTask) InfoText() string { return "Format (shfmt)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t shfmtTask) Run() error { args := []string{"bash", "-c", fmt.Sprintf(` shopt -s globstar ls **/*.sh || exit 0 %s **/*.sh `, shfmt.Name)} - if err := ciutil.RunCommand(args); err != nil { + if err := tools.RunCommand(args); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t shfmtTask) Post() error { return nil } -// InfoText implements [ciutil.Tasker.InfoText]. +// InfoText implements [tools.Tasker.InfoText]. func (t batsTask) InfoText() string { return "Test (bats)" } -// Run implements [ciutil.Tasker.Run]. +// Run implements [tools.Tasker.Run]. func (t batsTask) Run() error { args := []string{"bash", "-c", fmt.Sprintf(` shopt -s globstar @@ -78,12 +78,12 @@ func (t batsTask) Run() error { ls **/*.bats || exit 0 %s **/*.bats `, bats.Name)} - if err := ciutil.RunCommand(args); err != nil { + if err := tools.RunCommand(args); err != nil { return err } return nil } -// Post implements [ciutil.Tasker.Post]. +// Post implements [tools.Tasker.Post]. func (t batsTask) Post() error { return nil } diff --git a/internal/ci/shell/versions.go b/internal/tools/shell/versions.go similarity index 66% rename from internal/ci/shell/versions.go rename to internal/tools/shell/versions.go index f3ccb61..deedb7c 100644 --- a/internal/ci/shell/versions.go +++ b/internal/tools/shell/versions.go @@ -1,19 +1,19 @@ -package shellci +package shell import ( - ciutil "github.com/opensourcecorp/oscar/internal/ci/util" + "github.com/opensourcecorp/oscar/internal/tools" ) var ( - shellcheck = ciutil.Tool{ + shellcheck = tools.Tool{ Name: "shellcheck", Version: "v0.11.0", } - shfmt = ciutil.Tool{ + shfmt = tools.Tool{ Name: "shfmt", Version: "v3.12.0", } - bats = ciutil.Tool{ + bats = tools.Tool{ Name: "bats", // NOTE: bats just gets cloned then installed with its install script RemotePath: "https://github.com/bats-core/bats-core.git", diff --git a/internal/ci/configfiles/.markdownlint-cli2.yaml b/internal/tools/toolcfg/.markdownlint-cli2.yaml similarity index 100% rename from internal/ci/configfiles/.markdownlint-cli2.yaml rename to internal/tools/toolcfg/.markdownlint-cli2.yaml diff --git a/internal/tools/toolcfg/embed.go b/internal/tools/toolcfg/embed.go new file mode 100644 index 0000000..0796b85 --- /dev/null +++ b/internal/tools/toolcfg/embed.go @@ -0,0 +1,10 @@ +// Package toolcfg is used for storing embeddable config files for various tools, that are injected +// at runtime. +package toolcfg + +import "embed" + +// Files stores config files for each CI tool. +// +//go:embed *.conf *.toml *.yaml +var Files embed.FS diff --git a/internal/ci/configfiles/pyproject.toml b/internal/tools/toolcfg/pyproject.toml similarity index 100% rename from internal/ci/configfiles/pyproject.toml rename to internal/tools/toolcfg/pyproject.toml diff --git a/internal/ci/configfiles/revive.toml b/internal/tools/toolcfg/revive.toml similarity index 100% rename from internal/ci/configfiles/revive.toml rename to internal/tools/toolcfg/revive.toml diff --git a/internal/ci/configfiles/staticcheck.conf b/internal/tools/toolcfg/staticcheck.conf similarity index 100% rename from internal/ci/configfiles/staticcheck.conf rename to internal/tools/toolcfg/staticcheck.conf diff --git a/internal/ci/util/types.go b/internal/tools/types.go similarity index 99% rename from internal/ci/util/types.go rename to internal/tools/types.go index 05434d8..523ef35 100644 --- a/internal/ci/util/types.go +++ b/internal/tools/types.go @@ -1,4 +1,4 @@ -package ciutil +package tools // Tasker defines the method set for working with metadata for a given CI Task. type Tasker interface { diff --git a/internal/ci/util/util.go b/internal/tools/util.go similarity index 97% rename from internal/ci/util/util.go rename to internal/tools/util.go index 47cda77..b3018ef 100644 --- a/internal/ci/util/util.go +++ b/internal/tools/util.go @@ -1,4 +1,4 @@ -package ciutil +package tools import ( "errors" @@ -13,6 +13,7 @@ import ( "github.com/opensourcecorp/oscar" "github.com/opensourcecorp/oscar/internal/consts" + "github.com/opensourcecorp/oscar/internal/hostinfo" iprint "github.com/opensourcecorp/oscar/internal/print" ) @@ -176,14 +177,14 @@ func installMise() (err error) { miseVersion = consts.MiseVersion } - hostInput := HostInfoInput{ + hostInput := hostinfo.Input{ KernelLinux: "linux", KernelMacOS: "macos", ArchAMD64: "x64", ArchARM64: "arm64", } - host, err := GetHostInfo(hostInput) + host, err := hostinfo.Get(hostInput) if err != nil { return fmt.Errorf("getting host info during mise install: %w", err) } diff --git a/scripts/tag-release.sh b/scripts/tag-release.sh new file mode 100755 index 0000000..2d9a3de --- /dev/null +++ b/scripts/tag-release.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +################################################################################ +# This script portably manages tagging of the HEAD commit based on the contents +# of the VERSION file. It performs some heuristic checks to make sure tagging +# will behave as expected. +################################################################################ + +root="$(git rev-parse --show-toplevel)" + +current_git_branch="$(git rev-parse --abbrev-ref HEAD)" +latest_git_tag="$(git tag --list | tail -n1)" +current_listed_version="$(grep -v '#' "${root:-}"/VERSION)" + +if [[ -z "${current_git_branch:-}" ]] ; then + printf 'ERROR: unable to determine current git branch\n' > /dev/stderr + exit 1 +fi +if [[ -z "${latest_git_tag:-}" ]] ; then + printf 'ERROR: unable to determine latest git tag\n' > /dev/stderr + exit 1 +fi +if [[ -z "${current_listed_version:-}" ]] ; then + printf 'ERROR: unable to determine version specified in VERSION file\n' > /dev/stderr + exit 1 +fi + +printf 'Current git branch: %s\n' "${current_git_branch:-}" +printf 'Latest git tag: %s\n' "${latest_git_tag:-}" +printf 'Current version listed in VERSION file: %s\n' "${current_listed_version:-}" + +failures=() + +if [[ "${current_git_branch}" == 'main' ]] ; then + printf 'On main branch, will manage tag checks & creation\n' + + # Fail if we forgot to bump VERSION + if [[ "${latest_git_tag}" == "v${current_listed_version}" ]] ; then + printf 'ERROR: Identifier in VERSION still matches what is tagged on the main branch -- did you forget to update?\n' > /dev/stderr + failures+=('forgot-to-bump-VERSION') + fi + + # Fail if we forgot to bump versions across files + # + # Note the "|| echo ''"s -- this is because grep throws a non-zero exit if it + # can't find a match, and it kills the script + old_git_status="$(git status | grep -i -E 'modified' || echo '')" + make -s bump-versions old_version="${current_listed_version}" > /dev/null + new_git_status="$(git status | grep -i -E 'modified' || echo '')" + if [[ "$(diff <(echo "${old_git_status}") <(echo "${new_git_status}") | wc -l)" -gt 0 ]] ; then + printf 'ERROR: Files modified by version-bump check -- did you forget to update versions across the repo to match VERSION?\n' > /dev/stderr + failures+=('forgot-to-bump-other-versions') + fi + + if [[ "${#failures[@]}" -gt 0 ]] ; then + exit 1 + else + printf 'All checks passed, tagging & pushing new version: %s --> %s\n' "${latest_git_tag}" "${current_listed_version}" + git tag --force "v${current_listed_version}" + git push --tags + fi + +else + printf 'Not on main branch, nothing to do\n' + exit 0 +fi diff --git a/scripts/xbuild.sh b/scripts/xbuild.sh new file mode 100755 index 0000000..b0df7d3 --- /dev/null +++ b/scripts/xbuild.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +targets=$(go tool dist list | grep -E 'linux|darwin' | grep -E 'amd64|arm64') +printf 'Will build for:\n' +while read -r line ; do + printf '\t%s\n' "${line}" +done <<< "${targets}" + +for target in ${targets} ; do + GOOS=$(echo "${target}" | cut -d'/' -f1) + GOARCH=$(echo "${target}" | cut -d'/' -f2) + export GOOS GOARCH + + mkdir -p ./build + out="./build/oscar-${GOOS}-${GOARCH}" + printf "Building to %s\n" "${out}" + go build -o "${out}" ./cmd/oscar + chmod +x "${out}" +done From fedb9dbc213b64214ab11dd2d5535ae98c2c2064 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Mon, 1 Sep 2025 00:47:41 -0500 Subject: [PATCH 02/38] Start work on adding Delivery tasks, which caused a lot of reorg --- .gitignore | 2 + Makefile | 11 +- VERSION | 2 +- internal/cli/root.go | 29 ++++- internal/{ci/git.go => git/ci.go} | 47 ++++---- internal/git/deliver.go | 60 ++++++++++ internal/git/doc.go | 2 + internal/print/print.go | 12 +- internal/semver/semver.go | 87 +++++++++++--- internal/semver/semver_test.go | 40 +++---- internal/{ => tasks}/ci/doc.go | 0 internal/{ => tasks}/ci/run.go | 42 +++---- internal/tasks/delivery/doc.go | 2 + internal/tasks/delivery/run.go | 133 +++++++++++++++++++++ internal/tools/go/{tasks.go => ci.go} | 163 +++++++++++++++----------- internal/tools/go/deliver.go | 77 ++++++++++++ internal/tools/go/versions.go | 38 ------ internal/tools/markdown/tasks.go | 2 +- internal/tools/python/tasks.go | 4 +- internal/tools/shell/tasks.go | 21 ++-- internal/tools/types.go | 14 ++- internal/tools/util.go | 23 ++-- internal/tools/version/doc.go | 2 + internal/tools/version/version.go | 89 ++++++++++++++ mise.toml | 1 + scripts/xbuild.sh | 20 ---- 26 files changed, 672 insertions(+), 251 deletions(-) rename internal/{ci/git.go => git/ci.go} (74%) create mode 100644 internal/git/deliver.go create mode 100644 internal/git/doc.go rename internal/{ => tasks}/ci/doc.go (100%) rename internal/{ => tasks}/ci/run.go (81%) create mode 100644 internal/tasks/delivery/doc.go create mode 100644 internal/tasks/delivery/run.go rename internal/tools/go/{tasks.go => ci.go} (50%) create mode 100644 internal/tools/go/deliver.go delete mode 100644 internal/tools/go/versions.go create mode 100644 internal/tools/version/doc.go create mode 100644 internal/tools/version/version.go delete mode 100755 scripts/xbuild.sh diff --git a/.gitignore b/.gitignore index 798c450..e882d75 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ dist/ *.zip .oscar/ + +scratch/ diff --git a/Makefile b/Makefile index cf46f6a..dca6c99 100644 --- a/Makefile +++ b/Makefile @@ -35,15 +35,8 @@ ci-container: -t $(BINNAME)-test:latest \ . -build: clean - @mkdir -p ./build/$$($(RUN) go env GOOS)-$$($(RUN) go env GOARCH) - @$(RUN) go build -o ./build/$(BINNAME) $(BINPATH) - @printf 'built to %s\n' ./build/$(BINNAME) - -xbuild: clean - @bash ./scripts/xbuild.sh - -package: xbuild +deliver: ci + @$(RUN) go run ./cmd/$(BINNAME)/main.go deliver clean: @rm -rf \ diff --git a/VERSION b/VERSION index 6e8bf73..0ea3a94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0 diff --git a/internal/cli/root.go b/internal/cli/root.go index 152a96f..8f79312 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -8,9 +8,10 @@ import ( "strings" "github.com/opensourcecorp/oscar" - "github.com/opensourcecorp/oscar/internal/ci" "github.com/opensourcecorp/oscar/internal/consts" iprint "github.com/opensourcecorp/oscar/internal/print" + "github.com/opensourcecorp/oscar/internal/tasks/ci" + "github.com/opensourcecorp/oscar/internal/tasks/delivery" "github.com/urfave/cli/v3" ) @@ -20,6 +21,8 @@ const ( debugFlagName = "debug" ciCommandName = "ci" + + deliverCommandName = "deliver" ) // NewRootCmd defines & returns the CLI command used as oscar's entrypoint. @@ -42,6 +45,11 @@ func NewRootCmd() *cli.Command { Usage: "Runs CI tasks", Action: ciAction, }, + { + Name: deliverCommandName, + Usage: "Runs Delivery tasks", + Action: deliverAction, + }, }, } @@ -78,17 +86,30 @@ func rootAction(_ context.Context, cmd *cli.Command) error { maybeSetDebug(cmd) iprint.Debugf("oscar root command\n") _ = cli.ShowAppHelp(cmd) - return errors.New("\nERROR: oscar requires a subcommand") + return errors.New("\nERROR: oscar requires a valid subcommand") } -// rootAction defines the logic for oscar's ci subcommand. +// ciAction defines the logic for oscar's ci subcommand. func ciAction(_ context.Context, cmd *cli.Command) error { maybeSetDebug(cmd) iprint.Banner() iprint.Debugf("oscar ci subcommand\n") if err := ci.Run(); err != nil { - return fmt.Errorf("running CI: %w", err) + return fmt.Errorf("running CI tasks: %w", err) + } + + return nil +} + +// deliverAction defines the logic for oscar's deliver subcommand. +func deliverAction(_ context.Context, cmd *cli.Command) error { + maybeSetDebug(cmd) + iprint.Banner() + iprint.Debugf("oscar deliver subcommand\n") + + if err := delivery.Run(); err != nil { + return fmt.Errorf("running Delivery tasks: %w", err) } return nil diff --git a/internal/ci/git.go b/internal/git/ci.go similarity index 74% rename from internal/ci/git.go rename to internal/git/ci.go index aaf9ec3..02d64a9 100644 --- a/internal/ci/git.go +++ b/internal/git/ci.go @@ -1,20 +1,20 @@ -package ci +package git import ( "fmt" - "os/exec" "regexp" "slices" "strings" iprint "github.com/opensourcecorp/oscar/internal/print" + "github.com/opensourcecorp/oscar/internal/tools" ) -// Git defines metadata & behavior for Git interactions. -type Git struct { - // BaselineStatusForCI is used to check against when running CI checks, so that each CI task can +// CI defines metadata & behavior for CI tasks. +type CI struct { + // BaselineStatus is used to check against when running CI checks, so that each CI task can // see if it introduced changes. - BaselineStatusForCI Status + BaselineStatus Status // CurrentStatus is the latest-available Git status, which may differ from the baseline. CurrentStatus Status } @@ -25,20 +25,20 @@ type Status struct { UntrackedFiles []string } -// NewGit returns a snapshot of Git information available at call-time. -func NewGit() (*Git, error) { +// NewForCI returns Git information for CI tasks. +func NewForCI() (*CI, error) { status, err := getRawStatus() if err != nil { return nil, err } - return &Git{ - BaselineStatusForCI: status, + return &CI{ + BaselineStatus: status, }, nil } -// Update recalculates various Git metadata, respecting any existing baseline values set in [NewGit]. -func (g *Git) Update() error { +// Update recalculates various Git metadata, respecting any existing baseline values set in [NewForCI]. +func (g *CI) Update() error { status, err := getRawStatus() if err != nil { return fmt.Errorf("getting Git status: %w", err) @@ -48,14 +48,14 @@ func (g *Git) Update() error { diff := make([]string, 0) for _, line := range status.Diff { - if !slices.Contains(g.BaselineStatusForCI.Diff, line) { + if !slices.Contains(g.BaselineStatus.Diff, line) { filename := regexp.MustCompile(`^ [A-Z] `).ReplaceAllString(line, "") diff = append(diff, filename) } } for _, line := range status.UntrackedFiles { - if !slices.Contains(g.BaselineStatusForCI.UntrackedFiles, line) { + if !slices.Contains(g.BaselineStatus.UntrackedFiles, line) { filename := strings.ReplaceAll(line, "?? ", "") untrackedFiles = append(diff, filename) } @@ -70,18 +70,18 @@ func (g *Git) Update() error { } // StatusHasChanged informs the caller of whether or not the [Status] now differs from the baseline. -func (g *Git) StatusHasChanged() (bool, error) { +func (g *CI) StatusHasChanged() (bool, error) { if err := g.updateStatus(); err != nil { return false, err } iprint.Debugf("len(g.CurrentStatus.Diff) = %d\n", len(g.CurrentStatus.Diff)) - iprint.Debugf("len(g.BaselineStatusForCI.Diff) = %d\n", len(g.BaselineStatusForCI.Diff)) + iprint.Debugf("len(g.BaselineStatusForCI.Diff) = %d\n", len(g.BaselineStatus.Diff)) iprint.Debugf("len(g.CurrentStatus.UntrackedFiles) = %d\n", len(g.CurrentStatus.UntrackedFiles)) - iprint.Debugf("len(g.BaselineStatusForCI.UntrackedFiles) = %d\n", len(g.BaselineStatusForCI.UntrackedFiles)) + iprint.Debugf("len(g.BaselineStatusForCI.UntrackedFiles) = %d\n", len(g.BaselineStatus.UntrackedFiles)) - statusChanged := (len(g.CurrentStatus.Diff)+len(g.BaselineStatusForCI.Diff)) != len(g.BaselineStatusForCI.Diff) || - (len(g.CurrentStatus.UntrackedFiles)+len(g.BaselineStatusForCI.UntrackedFiles)) != len(g.BaselineStatusForCI.UntrackedFiles) + statusChanged := (len(g.CurrentStatus.Diff)+len(g.BaselineStatus.Diff)) != len(g.BaselineStatus.Diff) || + (len(g.CurrentStatus.UntrackedFiles)+len(g.BaselineStatus.UntrackedFiles)) != len(g.BaselineStatus.UntrackedFiles) iprint.Debugf("statusChanged: %v\n", statusChanged) @@ -91,8 +91,7 @@ func (g *Git) StatusHasChanged() (bool, error) { // getRawStatus returns a slightly-modified "git status" output, so that calling tools can parse it // more easily. func getRawStatus() (Status, error) { - cmd := exec.Command("git", "status", "--porcelain") - outputBytes, err := cmd.CombinedOutput() + outputBytes, err := tools.RunCommand([]string{"git", "status", "--porcelain"}) if err != nil { return Status{}, fmt.Errorf("getting git status output: %w", err) } @@ -122,7 +121,7 @@ func getRawStatus() (Status, error) { } // updateStatus updates the tracked Git status so that it can be compared against the baseline. -func (g *Git) updateStatus() error { +func (g *CI) updateStatus() error { // So any future debug logs have a line break in them iprint.Debugf("\n") @@ -138,14 +137,14 @@ func (g *Git) updateStatus() error { for _, line := range status.Diff { filename := regexp.MustCompile(`^( +)?[A-Z]+ +`).ReplaceAllString(line, "") - if !slices.Contains(g.BaselineStatusForCI.Diff, filename) { + if !slices.Contains(g.BaselineStatus.Diff, filename) { diff = append(diff, filename) } } for _, line := range status.UntrackedFiles { filename := strings.ReplaceAll(line, "?? ", "") - if !slices.Contains(g.BaselineStatusForCI.UntrackedFiles, filename) { + if !slices.Contains(g.BaselineStatus.UntrackedFiles, filename) { untrackedFiles = append(diff, filename) } } diff --git a/internal/git/deliver.go b/internal/git/deliver.go new file mode 100644 index 0000000..7ee08fd --- /dev/null +++ b/internal/git/deliver.go @@ -0,0 +1,60 @@ +package git + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + iprint "github.com/opensourcecorp/oscar/internal/print" + "github.com/opensourcecorp/oscar/internal/tools" +) + +// Delivery defines metadata & behavior for Delivery tasks. +type Delivery struct { + Root string + LatestTag string + // From {Root}/VERSION file + CurrentVersionFromFile string +} + +// NewForDelivery returns Git information for Delivery tasks. +func NewForDelivery() (*Delivery, error) { + root, err := tools.RunCommand([]string{"git", "rev-parse", "--show-toplevel"}) + if err != nil { + return nil, err + } + + latestTag, err := tools.RunCommand([]string{"bash", "-c", "git tag --list | tail -n1"}) + if err != nil { + return nil, err + } + + versionFileContents, err := os.ReadFile(filepath.Join(root, "VERSION")) + if err != nil { + return nil, err + } + + versionFileLines := strings.Split(string(versionFileContents), "\n") + var version string + for _, line := range versionFileLines { + if regexp.MustCompile(`[0-9]+\.[0-9]+\.[0-9]+`).MatchString(line) { + version = line + break + } + } + + if version == "" { + return nil, fmt.Errorf("could not determine a Semantic Version from your 'VERSION' file") + } + + out := &Delivery{ + Root: root, + LatestTag: latestTag, + CurrentVersionFromFile: version, + } + iprint.Debugf("git.Delivery: %+v\n", out) + + return out, nil +} diff --git a/internal/git/doc.go b/internal/git/doc.go new file mode 100644 index 0000000..00b2819 --- /dev/null +++ b/internal/git/doc.go @@ -0,0 +1,2 @@ +// Package git provides interoperability with Git. +package git diff --git a/internal/print/print.go b/internal/print/print.go index b3cd9fd..63a3e0d 100644 --- a/internal/print/print.go +++ b/internal/print/print.go @@ -10,12 +10,12 @@ import ( // Banner prints the oscar banner. func Banner() { var banner = ` - ____________________ - /____________________/| -| _ _ _ _ _ |/| -| | | |_ | |_| |_| |/| -| |_| _| |_ | | | \ |/| -|____________________|/ + ____________________ \ +=~=~=~=~=/____________________/|---------\ +~=~=~=~=| _ _ _ _ _ |/|----------\ +=~=~=~=~| | | |_ | |_| |_| |/|----------/ +~=~=~=~=| |_| _| |_ | | | \ |/|---------/ + |____________________|/ / ` fmt.Println(banner) } diff --git a/internal/semver/semver.go b/internal/semver/semver.go index b803f6e..1faea48 100644 --- a/internal/semver/semver.go +++ b/internal/semver/semver.go @@ -2,29 +2,43 @@ package semver import ( "fmt" + "os" + "path/filepath" "regexp" "strings" iprint "github.com/opensourcecorp/oscar/internal/print" - "golang.org/x/mod/semver" + "github.com/opensourcecorp/oscar/internal/tools" + xsemver "golang.org/x/mod/semver" ) -// GetSemver tries to build a (Go) compliant Semantic Version number out of the provided string, -// regardless of how dirty it is. Despite using the semver package in a few places internally, most -// of this implementation is custom due to limitations in that package -- like not being able to -// parse out just the Patch number, or Pre-Release/Build numbers not being allowed in +// Get tries to build a compliant Semantic Version number out of the provided string, regardless of +// how dirty it is. Despite using the "golang.org/x/mod/semver" package in a few places internally, +// most of this implementation is custom due to limitations in that package -- like not being able +// to parse out just the Patch number, or Pre-Release/Build numbers not being allowed in // semver.Canonical() -func GetSemver(s string) (string, error) { - var v, preRelease, build string - +func Get(s string) (string, error) { // Grab the semver parts separately so we can clean them up. Firstly, the Major-Minor-Patch // parts, but Patch takes some extra work to suss out -- if there's any prerelease or build // parts of the version, these show up in the "patch" index if we just split on dots, so we can // just grab the whole MMP with a regex - v = "v" + regexp.MustCompile(`\d+(\.\d+)?(\.\d+)?`).FindStringSubmatch(s)[0] - v = semver.Canonical(v) + matchList := regexp.MustCompile(`[0-9]+(\.[0-9]+)?(\.[0-9]+)?`).FindStringSubmatch(s) + if len(matchList) == 0 { + return "", fmt.Errorf("malformed or unmatchable Semantic Version number from 'VERSION' file (got: '%s')", s) + } + v := matchList[0] - // Gross + // NOTE: the external semver package has some niceties, and we use them here, but since it's a + // Go package it expects a "v" prefix on every number. We want to just keep the non-"v" data + // since it's more portable, so we need to self-prefix the version number for the remaining + // duration of this function. + v = xsemver.Canonical("v" + v) + if v == "" { + return "", fmt.Errorf("unable to canonicalize provided version '%s' (after possibly converting to '%s')", s, v) + } + + // Gross. + var preRelease, build string prSplit := strings.Split(s, "-") bSplit := strings.Split(s, "+") if len(prSplit) > 1 { @@ -42,13 +56,54 @@ func GetSemver(s string) (string, error) { v += "+" + build } - if !semver.IsValid(v) { - return "", fmt.Errorf("could not understand the semantic version you provided in your Oscarfile: '%s'", s) + if !xsemver.IsValid(v) { + return "", fmt.Errorf("could not understand the Semantic Version you provided in your 'VERSION' file (got: '%s', converted to: '%s')", s, v) } - if v != s { - iprint.Warnf("the Semantic Version string built was different from the one provided -- please edit your version to match the correct format: %s --> %s\n", s, v) - } + // NOW, we can finally strip off the "v" prefix + v = strings.TrimPrefix(v, "v") return v, nil } + +// GetFromFile reads the contents of the codebase's "VERSION" file, and passes it to [Get]. +func GetFromFile(pathOverride ...string) (string, error) { + root, err := tools.RunCommand([]string{"git", "rev-parse", "--show-toplevel"}) + if err != nil { + return "", err + } + + path := filepath.Join(root, "VERSION") + if len(pathOverride) > 0 { + path = pathOverride[0] + } + + versionFileContents, err := os.ReadFile(path) + if err != nil { + return "", err + } + + versionFileLines := strings.Split(string(versionFileContents), "\n") + var version string + for _, line := range versionFileLines { + if v, err := Get(line); err == nil { + version = v + break + } + iprint.Debugf("error matching line in VERSION file: %v\n", err) + } + + if version == "" { + return "", fmt.Errorf("could not determine a Semantic Version from your 'VERSION' file") + } + + return version, nil +} + +// VersionWasIncremented reports whether the newVersion is greater than the oldVersion. +func VersionWasIncremented(newVersion string, oldVersion string) bool { + compValue := xsemver.Compare("v"+newVersion, "v"+oldVersion) + iprint.Debugf("semver comparison value: %d\n", compValue) + + return compValue > 0 +} diff --git a/internal/semver/semver_test.go b/internal/semver/semver_test.go index b39f6ca..4a957be 100644 --- a/internal/semver/semver_test.go +++ b/internal/semver/semver_test.go @@ -5,48 +5,48 @@ import ( ) func TestGetSemver(t *testing.T) { - t.Run("Convert regular semver to be conformant", func(t *testing.T) { - s := "1.0.0.0" - want := "v1.0.0" - got, err := GetSemver(s) + t.Run("No conversion on a basic conformant semver", func(t *testing.T) { + s := "1.0.0" + want := "1.0.0" + got, err := Get(s) if err != nil { - t.Errorf("unexpected error") + t.Errorf("unexpected error: %v", err) } if want != got { t.Errorf("Expected version string '%s' to become '%s', but got '%s'\n", s, want, got) } }) - t.Run("Convert semver with prerelease info to be conformant", func(t *testing.T) { - s := "1.0-alpha" - want := "v1.0.0-alpha" - got, err := GetSemver(s) + t.Run("No conversion on a full conformant semver", func(t *testing.T) { + s := "1.1.9-prebeta1+abc" + want := s + got, err := Get(s) if err != nil { - t.Errorf("unexpected error") + t.Errorf("unexpected error: %v", err) } if want != got { t.Errorf("Expected version string '%s' to become '%s', but got '%s'\n", s, want, got) } }) - t.Run("Convert semver with build info to be conformant", func(t *testing.T) { - s := "1.0.2+abc" - want := "v1.0.2+abc" - got, err := GetSemver(s) + t.Run("Convert semver with prerelease info to be conformant", func(t *testing.T) { + s := "1.0-alpha" + want := "1.0.0-alpha" + got, err := Get(s) if err != nil { - t.Errorf("unexpected error") + t.Errorf("unexpected error: %v", err) } if want != got { t.Errorf("Expected version string '%s' to become '%s', but got '%s'\n", s, want, got) } }) - t.Run("No conversion on a conformant semver", func(t *testing.T) { - s := "v1.1.9-prebeta1+abc" - want := s - got, err := GetSemver(s) + t.Run("Convert semver with build info to be conformant", func(t *testing.T) { + s := "1.0+abc" + want := "1.0.0+abc" + got, err := Get(s) if err != nil { - t.Errorf("unexpected error") + t.Errorf("unexpected error: %v", err) } if want != got { t.Errorf("Expected version string '%s' to become '%s', but got '%s'\n", s, want, got) diff --git a/internal/ci/doc.go b/internal/tasks/ci/doc.go similarity index 100% rename from internal/ci/doc.go rename to internal/tasks/ci/doc.go diff --git a/internal/ci/run.go b/internal/tasks/ci/run.go similarity index 81% rename from internal/ci/run.go rename to internal/tasks/ci/run.go index a0387ee..7c509db 100644 --- a/internal/ci/run.go +++ b/internal/tasks/ci/run.go @@ -8,31 +8,30 @@ import ( "time" "github.com/opensourcecorp/oscar/internal/consts" + "github.com/opensourcecorp/oscar/internal/git" iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/tools" igo "github.com/opensourcecorp/oscar/internal/tools/go" "github.com/opensourcecorp/oscar/internal/tools/markdown" "github.com/opensourcecorp/oscar/internal/tools/python" "github.com/opensourcecorp/oscar/internal/tools/shell" + "github.com/opensourcecorp/oscar/internal/tools/version" ) -// TaskMap is a less-verbose type alias for mapping language names to function signatures that -// return a language's tasks. -type TaskMap map[string][]tools.Tasker - // GetCITaskMap assembles the overall list of CI tasks, keyed by their language/tooling name -func GetCITaskMap() (TaskMap, error) { +func GetCITaskMap() (tools.TaskMap, error) { repo, err := tools.GetRepoComposition() if err != nil { return nil, fmt.Errorf("getting repo composition: %w", err) } - out := make(TaskMap, 0) + out := make(tools.TaskMap, 0) for langName, getTasksFunc := range map[string]func(tools.Repo) []tools.Tasker{ - "Go": igo.Tasks, - "Python": python.Tasks, - "Shell": shell.Tasks, - "Markdown": markdown.Tasks, + "VERSION file": version.TasksForCI, + "Go": igo.TasksForCI, + "Python": python.Tasks, + "Shell": shell.Tasks, + "Markdown": markdown.Tasks, } { tasks := getTasksFunc(repo) if len(tasks) > 0 { @@ -42,7 +41,7 @@ func GetCITaskMap() (TaskMap, error) { if len(out) > 0 { fmt.Print(repo.String()) - iprint.Debugf("GetCITasks output: %+v\n", out) + iprint.Debugf("GetCITasks output: %#v\n", out) } return out, nil @@ -88,7 +87,7 @@ func Run() (err error) { iprint.Debugf("longestInfoTextLength: %d\n", longestInfoTextLength) // For tracking any changes to Git status etc. after each Task runs - git, err := NewGit() + gitCI, err := git.NewForCI() if err != nil { return fmt.Errorf("internal error: %w", err) } @@ -98,12 +97,13 @@ func Run() (err error) { for lang, tasks := range ciTaskMap { langNameBannerPadding := strings.Repeat("=", longestLanguageNameLength-len(lang)/2) fmt.Printf( - "============%s %s %s============\n", - langNameBannerPadding, lang, langNameBannerPadding, + "%s%s %s %s%s\n", + strings.Repeat("=", 24), langNameBannerPadding, lang, langNameBannerPadding, strings.Repeat("=", 24), ) for _, t := range tasks { - // NOTE: if no InfoText() method is provided, it's probably a lang-wide init func, so skip it + // NOTE: if no InfoText() method is provided, it's probably a lang-wide init func, so + // skip it if t.InfoText() == "" { continue } @@ -120,16 +120,16 @@ func Run() (err error) { runErr = errors.Join(runErr, t.Run()) runErr = errors.Join(runErr, t.Post()) - if err := git.Update(); err != nil { + if err := gitCI.Update(); err != nil { return fmt.Errorf("internal error: %w", err) } - gitStatusHasChanged, err := git.StatusHasChanged() + gitStatusHasChanged, err := gitCI.StatusHasChanged() if err != nil { return fmt.Errorf("internal error: %w", err) } if runErr != nil || gitStatusHasChanged { - iprint.Errorf("FAILED! (%s)\n", tools.RunDurationString(taskStartTime)) + iprint.Errorf("FAILED (%s)\n", tools.RunDurationString(taskStartTime)) iprint.Errorf("\n") if runErr != nil { @@ -137,15 +137,15 @@ func Run() (err error) { } if gitStatusHasChanged { - iprint.Errorf("Files ~CHANGED~ during run: %+v\n", git.CurrentStatus.Diff) - iprint.Errorf("Files +CREATED+ during run: %+v\n", git.CurrentStatus.UntrackedFiles) + iprint.Errorf("Files ~CHANGED~ during run: %+v\n", gitCI.CurrentStatus.Diff) + iprint.Errorf("Files +CREATED+ during run: %+v\n", gitCI.CurrentStatus.UntrackedFiles) iprint.Errorf("\n") } failures = append(failures, fmt.Sprintf("%s :: %s", lang, t.InfoText())) // Also need to reset the baseline status - git, err = NewGit() + gitCI, err = git.NewForCI() if err != nil { return fmt.Errorf("internal error: %w", err) } diff --git a/internal/tasks/delivery/doc.go b/internal/tasks/delivery/doc.go new file mode 100644 index 0000000..a3930d6 --- /dev/null +++ b/internal/tasks/delivery/doc.go @@ -0,0 +1,2 @@ +// Package delivery defines behavior for running delivery tasks. +package delivery diff --git a/internal/tasks/delivery/run.go b/internal/tasks/delivery/run.go new file mode 100644 index 0000000..41d8d63 --- /dev/null +++ b/internal/tasks/delivery/run.go @@ -0,0 +1,133 @@ +package delivery + +import ( + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/opensourcecorp/oscar/internal/consts" + iprint "github.com/opensourcecorp/oscar/internal/print" + "github.com/opensourcecorp/oscar/internal/tools" + igo "github.com/opensourcecorp/oscar/internal/tools/go" +) + +// GetDeliveryTaskMap assembles the overall list of Delivery tasks, keyed by their language/tooling +// name. +func GetDeliveryTaskMap() (tools.TaskMap, error) { + repo, err := tools.GetRepoComposition() + if err != nil { + return nil, fmt.Errorf("getting repo composition: %w", err) + } + + out := make(tools.TaskMap, 0) + for langName, getTasksFunc := range map[string]func(tools.Repo) []tools.Tasker{ + "Go": igo.TasksForDelivery, + } { + tasks := getTasksFunc(repo) + if len(tasks) > 0 { + out[langName] = tasks + } + } + + if len(out) > 0 { + fmt.Print(repo.String()) + iprint.Debugf("GetDeliveryTaskMap output: %#v\n", out) + } + + return out, nil +} + +// Run defines the behavior for running all Delivery tasks for the repository. +func Run() (err error) { + runStartTime := time.Now() + + // Handle system init + if err := tools.InitSystem(); err != nil { + return fmt.Errorf("initializing system: %w", err) + } + // The mise config that oscar uses is written during init, so be sure to defer its removal here + defer func() { + if rmErr := os.Remove(consts.MiseConfigFileName); rmErr != nil { + err = errors.Join(err, fmt.Errorf("removing mise config file: %w", rmErr)) + } + }() + + var ( + // Vars for determining text padding in output banners + longestLanguageNameLength int + longestInfoTextLength int + ) + + // All the Delivery tasks that will be looped over. Will also print a summary of discovered file + // types. + deliveryTaskMap, err := GetDeliveryTaskMap() + if err != nil { + return fmt.Errorf("getting Delivery tasks: %w", err) + } + + // Log padding setup + for lang, tasks := range deliveryTaskMap { + longestLanguageNameLength = max(longestLanguageNameLength, len(lang)) + for _, t := range tasks { + longestInfoTextLength = max(longestInfoTextLength, len(t.InfoText())) + } + } + + iprint.Debugf("longestLanguageNameLength: %d\n", longestLanguageNameLength) + iprint.Debugf("longestInfoTextLength: %d\n", longestInfoTextLength) + + // Keeps track of all task failures + failures := make([]string, 0) + for lang, tasks := range deliveryTaskMap { + langNameBannerPadding := strings.Repeat("=", longestLanguageNameLength-len(lang)/2) + fmt.Printf( + "%s%s %s %s%s\n", + strings.Repeat("=", 24), langNameBannerPadding, lang, langNameBannerPadding, strings.Repeat("=", 24), + ) + + for _, t := range tasks { + // NOTE: if no InfoText() method is provided, it's probably a lang-wide init func, so + // skip it + if t.InfoText() == "" { + continue + } + + taskStartTime := time.Now() + + taskBannerPadding := strings.Repeat(".", longestInfoTextLength-len(t.InfoText())) + // NOTE: no trailing newline on purpose + fmt.Printf("> %s %s............", t.InfoText(), taskBannerPadding) + + // NOTE: this error is checked later, when we can check the Run, Post, and git-diff + // potential errors together + var runErr error + runErr = errors.Join(runErr, t.Run()) + runErr = errors.Join(runErr, t.Post()) + + if runErr != nil { + iprint.Errorf("FAILED (%s)\n", tools.RunDurationString(taskStartTime)) + iprint.Errorf("%v\n", runErr) + + failures = append(failures, fmt.Sprintf("%s :: %s", lang, t.InfoText())) + } else { + fmt.Printf("SUCCEEDED (%s)\n", tools.RunDurationString(taskStartTime)) + } + } + } + + if len(failures) > 0 { + iprint.Errorf("\n================================================================\n") + iprint.Errorf("The following tasks failed: (%s)\n", tools.RunDurationString(runStartTime)) + for _, f := range failures { + iprint.Errorf("- %s\n", f) + } + iprint.Errorf("================================================================\n\n") + return errors.New("one or more Delivery tasks failed") + } + + fmt.Printf("All tasks succeeded! (%s)\n", tools.RunDurationString(runStartTime)) + + return err +} diff --git a/internal/tools/go/tasks.go b/internal/tools/go/ci.go similarity index 50% rename from internal/tools/go/tasks.go rename to internal/tools/go/ci.go index eaa564d..cda561d 100644 --- a/internal/tools/go/tasks.go +++ b/internal/tools/go/ci.go @@ -10,50 +10,79 @@ import ( "github.com/opensourcecorp/oscar/internal/tools/toolcfg" ) -// A list of tasks that all implement [tools.Tasker], for Go. type ( - goModCheckTask struct{} - goFormatTask struct{} - generateCodeTask struct{} - goBuildTask struct{} - goVetTask struct{} - staticcheckTask struct{} - reviveTask struct{} - errcheckTask struct{} - goImportsTask struct{} - govulncheckTask struct{} - goTestTask struct{} + goModCheckCI struct{} + goFormatCI struct{} + generateCodeCI struct{} + goBuildCI struct{} + goVetCI struct{} + staticcheckCI struct{} + reviveCI struct{} + errcheckCI struct{} + goImportsCI struct{} + govulncheckCI struct{} + goTestCI struct{} ) -var tasks = []tools.Tasker{ - goModCheckTask{}, - goFormatTask{}, - generateCodeTask{}, - goBuildTask{}, - goVetTask{}, - staticcheckTask{}, - reviveTask{}, - errcheckTask{}, - goImportsTask{}, - govulncheckTask{}, - goTestTask{}, +var ciTasks = []tools.Tasker{ + goModCheckCI{}, + goFormatCI{}, + generateCodeCI{}, + goBuildCI{}, + goVetCI{}, + staticcheckCI{}, + reviveCI{}, + errcheckCI{}, + goImportsCI{}, + govulncheckCI{}, + goTestCI{}, } -// Tasks returns the list of CI tasks. -func Tasks(repo tools.Repo) []tools.Tasker { +var ( + staticcheck = tools.Tool{ + Name: "staticcheck", + RemotePath: "honnef.co/go/tools/cmd/staticcheck", + Version: "2025.1.1", + ConfigFilePath: filepath.Join("./staticcheck.conf"), + } + revive = tools.Tool{ + Name: "revive", + RemotePath: "github.com/mgechev/revive", + Version: "v1.11.0", + ConfigFilePath: filepath.Join(os.TempDir(), "revive.toml"), + } + errcheck = tools.Tool{ + Name: "errcheck", + RemotePath: "github.com/kisielk/errcheck", + Version: "v1.9.0", + } + goimports = tools.Tool{ + Name: "goimports", + RemotePath: "golang.org/x/tools/cmd/goimports", + Version: "v0.35.0", + } + govulncheck = tools.Tool{ + Name: "govulncheck", + RemotePath: "golang.org/x/vuln/cmd/govulncheck", + Version: "v1.1.4", + } +) + +// TasksForCI returns the list of CI tasks. +func TasksForCI(repo tools.Repo) []tools.Tasker { if repo.HasGo { - return tasks + return ciTasks } return nil } // InfoText implements [tools.Tasker.InfoText]. -func (t goModCheckTask) InfoText() string { return "go.mod tidy check" } +func (t goModCheckCI) InfoText() string { return "go.mod tidy check" } // Run implements [tools.Tasker.Run]. -func (t goModCheckTask) Run() error { - if err := tools.RunCommand([]string{"go", "mod", "tidy"}); err != nil { +func (t goModCheckCI) Run() error { + if _, err := tools.RunCommand([]string{"go", "mod", "tidy"}); err != nil { return err } @@ -61,14 +90,14 @@ func (t goModCheckTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goModCheckTask) Post() error { return nil } +func (t goModCheckCI) Post() error { return nil } // InfoText implements [tools.Tasker.InfoText]. -func (t goFormatTask) InfoText() string { return "Format" } +func (t goFormatCI) InfoText() string { return "Format" } // Run implements [tools.Tasker.Run]. -func (t goFormatTask) Run() error { - if err := tools.RunCommand([]string{"go", "fmt", "./..."}); err != nil { +func (t goFormatCI) Run() error { + if _, err := tools.RunCommand([]string{"go", "fmt", "./..."}); err != nil { return err } @@ -76,14 +105,14 @@ func (t goFormatTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goFormatTask) Post() error { return nil } +func (t goFormatCI) Post() error { return nil } // InfoText implements [tools.Tasker.InfoText]. -func (t generateCodeTask) InfoText() string { return "Generate code" } +func (t generateCodeCI) InfoText() string { return "Generate code" } // Run implements [tools.Tasker.Run]. -func (t generateCodeTask) Run() error { - if err := tools.RunCommand([]string{"go", "generate", "./..."}); err != nil { +func (t generateCodeCI) Run() error { + if _, err := tools.RunCommand([]string{"go", "generate", "./..."}); err != nil { return err } @@ -91,14 +120,14 @@ func (t generateCodeTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t generateCodeTask) Post() error { return nil } +func (t generateCodeCI) Post() error { return nil } // InfoText implements [tools.Tasker.InfoText]. -func (t goBuildTask) InfoText() string { return "Build" } +func (t goBuildCI) InfoText() string { return "Build" } // Run implements [tools.Tasker.Run]. -func (t goBuildTask) Run() error { - if err := tools.RunCommand([]string{"go", "build", "./..."}); err != nil { +func (t goBuildCI) Run() error { + if _, err := tools.RunCommand([]string{"go", "build", "./..."}); err != nil { return err } @@ -106,14 +135,14 @@ func (t goBuildTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goBuildTask) Post() error { return nil } +func (t goBuildCI) Post() error { return nil } // InfoText implements [tools.Tasker.InfoText]. -func (t goVetTask) InfoText() string { return "Vet" } +func (t goVetCI) InfoText() string { return "Vet" } // Run implements [tools.Tasker.Run]. -func (t goVetTask) Run() error { - if err := tools.RunCommand([]string{"go", "vet", "./..."}); err != nil { +func (t goVetCI) Run() error { + if _, err := tools.RunCommand([]string{"go", "vet", "./..."}); err != nil { return err } @@ -121,13 +150,13 @@ func (t goVetTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goVetTask) Post() error { return nil } +func (t goVetCI) Post() error { return nil } // InfoText implements [tools.Tasker.InfoText]. -func (t staticcheckTask) InfoText() string { return "Lint (staticcheck)" } +func (t staticcheckCI) InfoText() string { return "Lint (staticcheck)" } // Run implements [tools.Tasker.Run]. -func (t staticcheckTask) Run() (err error) { +func (t staticcheckCI) Run() (err error) { cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(staticcheck.ConfigFilePath)) if err != nil { return fmt.Errorf("reading embedded file contents: %w", err) @@ -145,7 +174,7 @@ func (t staticcheckTask) Run() (err error) { } // Post implements [tools.Tasker.Post]. -func (t staticcheckTask) Post() error { +func (t staticcheckCI) Post() error { if err := os.RemoveAll(staticcheck.ConfigFilePath); err != nil { return fmt.Errorf("removing config file: %w", err) } @@ -154,10 +183,10 @@ func (t staticcheckTask) Post() error { } // InfoText implements [tools.Tasker.InfoText]. -func (t reviveTask) InfoText() string { return "Lint (revive)" } +func (t reviveCI) InfoText() string { return "Lint (revive)" } // Run implements [tools.Tasker.Run]. -func (t reviveTask) Run() error { +func (t reviveCI) Run() error { cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(revive.ConfigFilePath)) if err != nil { return fmt.Errorf("reading embedded file contents: %w", err) @@ -181,7 +210,7 @@ func (t reviveTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t reviveTask) Post() error { +func (t reviveCI) Post() error { if err := os.RemoveAll(revive.ConfigFilePath); err != nil { return fmt.Errorf("removing config file: %w", err) } @@ -190,10 +219,10 @@ func (t reviveTask) Post() error { } // InfoText implements [tools.Tasker.InfoText]. -func (t errcheckTask) InfoText() string { return "Lint (errcheck)" } +func (t errcheckCI) InfoText() string { return "Lint (errcheck)" } // Run implements [tools.Tasker.Run]. -func (t errcheckTask) Run() error { +func (t errcheckCI) Run() error { if err := goRun(errcheck, "./..."); err != nil { return err } @@ -202,13 +231,13 @@ func (t errcheckTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t errcheckTask) Post() error { return nil } +func (t errcheckCI) Post() error { return nil } // InfoText implements [tools.Tasker.InfoText]. -func (t goImportsTask) InfoText() string { return "Format imports" } +func (t goImportsCI) InfoText() string { return "Format imports" } // Run implements [tools.Tasker.Run]. -func (t goImportsTask) Run() error { +func (t goImportsCI) Run() error { args := []string{"-l", "-w", "."} if err := goRun(goimports, args...); err != nil { return err @@ -218,13 +247,13 @@ func (t goImportsTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goImportsTask) Post() error { return nil } +func (t goImportsCI) Post() error { return nil } // InfoText implements [tools.Tasker.InfoText]. -func (t govulncheckTask) InfoText() string { return "Vulnerability scan (govulncheck)" } +func (t govulncheckCI) InfoText() string { return "Vulnerability scan (govulncheck)" } // Run implements [tools.Tasker.Run]. -func (t govulncheckTask) Run() error { +func (t govulncheckCI) Run() error { if err := goRun(govulncheck, "./..."); err != nil { return err } @@ -233,14 +262,14 @@ func (t govulncheckTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t govulncheckTask) Post() error { return nil } +func (t govulncheckCI) Post() error { return nil } // InfoText implements [tools.Tasker.InfoText]. -func (t goTestTask) InfoText() string { return "Test" } +func (t goTestCI) InfoText() string { return "Test" } // Run implements [tools.Tasker.Run]. -func (t goTestTask) Run() error { - if err := tools.RunCommand([]string{"go", "test", "./..."}); err != nil { +func (t goTestCI) Run() error { + if _, err := tools.RunCommand([]string{"go", "test", "./..."}); err != nil { return err } @@ -248,7 +277,7 @@ func (t goTestTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goTestTask) Post() error { return nil } +func (t goTestCI) Post() error { return nil } // goRun is a wrapper for "go run" func goRun(t tools.Tool, trailingArgs ...string) error { @@ -256,7 +285,7 @@ func goRun(t tools.Tool, trailingArgs ...string) error { []string{"go", "run", fmt.Sprintf("%s@%s", t.RemotePath, t.Version)}, trailingArgs, ) - if err := tools.RunCommand(args); err != nil { + if _, err := tools.RunCommand(args); err != nil { return fmt.Errorf("running 'go run': %w", err) } diff --git a/internal/tools/go/deliver.go b/internal/tools/go/deliver.go new file mode 100644 index 0000000..f6efebd --- /dev/null +++ b/internal/tools/go/deliver.go @@ -0,0 +1,77 @@ +package igo + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/opensourcecorp/oscar/internal/tools" +) + +type ( + ghRelease struct{} +) + +var deliveryTasks = []tools.Tasker{ + ghRelease{}, +} + +// TasksForDelivery returns the list of Delivery tasks. +func TasksForDelivery(repo tools.Repo) []tools.Tasker { + if repo.HasGo { + return deliveryTasks + } + + return nil +} + +// InfoText implements [tools.Tasker.InfoText]. +func (t ghRelease) InfoText() string { return "GitHub Release" } + +// Run implements [tools.Tasker.Run]. +func (t ghRelease) Run() error { + targetDir := "build" + + if err := os.RemoveAll(targetDir); err != nil { + return fmt.Errorf("removing build directory: %w", err) + } + + if err := os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("creating build directory: %w", err) + } + + distros := []string{ + "linux/amd64", + "linux/arm64", + "darwin/amd64", + "darwin/arm64", + } + + for _, distro := range distros { + splits := strings.Split(distro, "/") + goos := splits[0] + goarch := splits[1] + + binName := "oscar" + + src := "./cmd/oscar" + target := filepath.Join(targetDir, fmt.Sprintf("%s-%s-%s", binName, goos, goarch)) + + if _, err := tools.RunCommand([]string{"bash", "-c", fmt.Sprintf(` + GOOS=%s GOARCH=%s go build -o %s %s`, + goos, goarch, target, src, + )}); err != nil { + return fmt.Errorf("building Go binary: %w", err) + } + + if err := os.Chmod(target, 0755); err != nil { + return fmt.Errorf("marking target as executable: %w", err) + } + } + + return nil +} + +// Post implements [tools.Tasker.Post]. +func (t ghRelease) Post() error { return nil } diff --git a/internal/tools/go/versions.go b/internal/tools/go/versions.go deleted file mode 100644 index c01ec3e..0000000 --- a/internal/tools/go/versions.go +++ /dev/null @@ -1,38 +0,0 @@ -package igo - -import ( - "os" - "path/filepath" - - "github.com/opensourcecorp/oscar/internal/tools" -) - -var ( - staticcheck = tools.Tool{ - Name: "staticcheck", - RemotePath: "honnef.co/go/tools/cmd/staticcheck", - Version: "2025.1.1", - ConfigFilePath: filepath.Join("./staticcheck.conf"), - } - revive = tools.Tool{ - Name: "revive", - RemotePath: "github.com/mgechev/revive", - Version: "v1.11.0", - ConfigFilePath: filepath.Join(os.TempDir(), "revive.toml"), - } - errcheck = tools.Tool{ - Name: "errcheck", - RemotePath: "github.com/kisielk/errcheck", - Version: "v1.9.0", - } - goimports = tools.Tool{ - Name: "goimports", - RemotePath: "golang.org/x/tools/cmd/goimports", - Version: "v0.35.0", - } - govulncheck = tools.Tool{ - Name: "govulncheck", - RemotePath: "golang.org/x/vuln/cmd/govulncheck", - Version: "v1.1.4", - } -) diff --git a/internal/tools/markdown/tasks.go b/internal/tools/markdown/tasks.go index 4bb8ed4..8c4e541 100644 --- a/internal/tools/markdown/tasks.go +++ b/internal/tools/markdown/tasks.go @@ -46,7 +46,7 @@ func (t markdownlintTask) Run() error { "**/*.md", } - if err := tools.RunCommand(args); err != nil { + if _, err := tools.RunCommand(args); err != nil { return err } diff --git a/internal/tools/python/tasks.go b/internal/tools/python/tasks.go index 27557ca..7086dab 100644 --- a/internal/tools/python/tasks.go +++ b/internal/tools/python/tasks.go @@ -52,7 +52,7 @@ func (t buildTask) InfoText() string { return "Build" } // Run implements [tools.Tasker.Run]. func (t buildTask) Run() error { - if err := tools.RunCommand([]string{"uv", "build"}); err != nil { + if _, err := tools.RunCommand([]string{"uv", "build"}); err != nil { return err } @@ -127,7 +127,7 @@ func pyRun(t tools.Tool, trailingArgs ...string) error { []string{"uvx", fmt.Sprintf("%s@%s", t.Name, t.Version)}, trailingArgs, ) - if err := tools.RunCommand(args); err != nil { + if _, err := tools.RunCommand(args); err != nil { return fmt.Errorf("running 'uvx': %w", err) } diff --git a/internal/tools/shell/tasks.go b/internal/tools/shell/tasks.go index f151a8f..8365ad1 100644 --- a/internal/tools/shell/tasks.go +++ b/internal/tools/shell/tasks.go @@ -35,9 +35,10 @@ func (t shellcheckTask) Run() error { args := []string{"bash", "-c", fmt.Sprintf(` shopt -s globstar ls **/*.sh || exit 0 - %s **/*.sh - `, shellcheck.Name)} - if err := tools.RunCommand(args); err != nil { + %s **/*.sh`, + shellcheck.Name, + )} + if _, err := tools.RunCommand(args); err != nil { return err } @@ -55,9 +56,10 @@ func (t shfmtTask) Run() error { args := []string{"bash", "-c", fmt.Sprintf(` shopt -s globstar ls **/*.sh || exit 0 - %s **/*.sh - `, shfmt.Name)} - if err := tools.RunCommand(args); err != nil { + %s **/*.sh`, + shfmt.Name, + )} + if _, err := tools.RunCommand(args); err != nil { return err } @@ -76,9 +78,10 @@ func (t batsTask) Run() error { shopt -s globstar # Don't run if no bats files found, otherwise it will error out ls **/*.bats || exit 0 - %s **/*.bats - `, bats.Name)} - if err := tools.RunCommand(args); err != nil { + %s **/*.bats`, + bats.Name, + )} + if _, err := tools.RunCommand(args); err != nil { return err } diff --git a/internal/tools/types.go b/internal/tools/types.go index 523ef35..db76167 100644 --- a/internal/tools/types.go +++ b/internal/tools/types.go @@ -20,8 +20,16 @@ type Repo struct { HasMarkdown bool } -// A Tool is a helper struct used to help other types implementing [Tasker] pass around their tool -// versioning/installation information. +// TaskMap is a less-verbose type alias for mapping language names to function signatures that +// return a language's tasks. +type TaskMap map[string][]Tasker + +// A Tool defines information about a tool used for running oscar's tasks. A Tool should be defined +// if a language etc. cannot perform the task itself. For example, you would not need a Tool to +// represent a task that runs "go test", but you *would* need a tool to represent a task that runs +// the external "staticcheck" linter for Go. +// +// Every Tool should implement [Tasker]. type Tool struct { // The tool's name, used as an identifier. May also be the tool's invocable command, in which // case it can be interpolated as such. @@ -39,7 +47,7 @@ type Tool struct { func (repo Repo) String() string { var out string - out += "The following file types were found in this repo, and CI checks will be run against them:\n" + out += "The following file types were found in this repo, and tasks can/will be run against them:\n" if repo.HasGo { out += "- Go\n" diff --git a/internal/tools/util.go b/internal/tools/util.go index b3018ef..0732e1b 100644 --- a/internal/tools/util.go +++ b/internal/tools/util.go @@ -69,10 +69,10 @@ func InitSystem() error { } // Init for task runs - if err := RunCommand([]string{consts.MiseBinPath, "trust", consts.MiseConfigFileName}); err != nil { + if _, err := RunCommand([]string{consts.MiseBinPath, "trust", consts.MiseConfigFileName}); err != nil { return fmt.Errorf("running mise trust: %w", err) } - if err := RunCommand([]string{consts.MiseBinPath, "install"}); err != nil { + if _, err := RunCommand([]string{consts.MiseBinPath, "install"}); err != nil { return fmt.Errorf("running mise install: %w", err) } @@ -82,10 +82,11 @@ func InitSystem() error { } // RunCommand takes a string slice containing an entire command & its args to run, and returns a -// consisten error message in case of failure. -func RunCommand(cmdArgs []string) error { +// consistent error message in case of failure. It also returns the command output, in case the +// caller needs to parse it on their own. +func RunCommand(cmdArgs []string) (string, error) { if len(cmdArgs) <= 1 { - return fmt.Errorf("internal error: not enough arguments passed to RunCommand() -- received: %v", cmdArgs) + return "", fmt.Errorf("internal error: not enough arguments passed to RunCommand() -- received: %v", cmdArgs) } var args []string @@ -97,14 +98,15 @@ func RunCommand(cmdArgs []string) error { cmd := exec.Command(consts.MiseBinPath, args...) iprint.Debugf("Running '%v'\n", cmd.Args) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf( + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf( "running '%v': %w, with output:\n%s", cmd.Args, err, string(output), ) } - return nil + return strings.TrimSuffix(string(output), "\n"), nil } // GetRepoComposition returns a populated [Repo]. @@ -234,8 +236,9 @@ func installMise() (err error) { func filesExistInTree(findScript string) (bool, error) { cmd := exec.Command("bash", "-c", fmt.Sprintf(` shopt -s globstar - %s - `, findScript)) + %s`, + findScript, + )) output, err := cmd.CombinedOutput() if err != nil { // If no files found, that's fine, just report it diff --git a/internal/tools/version/doc.go b/internal/tools/version/doc.go new file mode 100644 index 0000000..88f2965 --- /dev/null +++ b/internal/tools/version/doc.go @@ -0,0 +1,2 @@ +// Package version contains logic for running tasks against the codebase version identifier(s). +package version diff --git a/internal/tools/version/version.go b/internal/tools/version/version.go new file mode 100644 index 0000000..e5f4c2f --- /dev/null +++ b/internal/tools/version/version.go @@ -0,0 +1,89 @@ +package version + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + iprint "github.com/opensourcecorp/oscar/internal/print" + "github.com/opensourcecorp/oscar/internal/semver" + "github.com/opensourcecorp/oscar/internal/tools" +) + +type versionCI struct{} + +var tasks = []tools.Tasker{ + versionCI{}, +} + +// TasksForCI returns the list of CI tasks. +func TasksForCI(_ tools.Repo) []tools.Tasker { + return tasks +} + +// InfoText implements [tools.Tasker.InfoText]. +func (t versionCI) InfoText() string { return "VERSION file checks" } + +// Run implements [tools.Tasker.Run]. +func (t versionCI) Run() (err error) { + version, err := semver.GetFromFile() + if err != nil { + return err + } + iprint.Debugf("VERSION: %s\n", version) + + // NOTE: we clone the repo in question to a temp location to check the VERSION file contents on + // the main branch, instead of e.g. trying to checkout main. This is for may reasons, not the + // least of which being that alternatives would be unreliable in e.g. GitHub Actions CI based on + // how it treats PR checkouts et al. A small price to pay for reliability. + tmpCloneDir := filepath.Join(os.TempDir(), "oscar-ci", "this-repo") + if err := os.MkdirAll(filepath.Dir(tmpCloneDir), 0755); err != nil { + return fmt.Errorf("creating temp clone parent directory: %w", err) + } + defer func() { + if rmErr := os.RemoveAll(tmpCloneDir); rmErr != nil { + err = errors.Join(err, fmt.Errorf("removing temp clone directory: %w", rmErr)) + } + }() + + remote, err := tools.RunCommand([]string{"git", "remote", "get-url", "origin"}) + if err != nil { + return fmt.Errorf("determining git root: %w", err) + } + + if _, err := tools.RunCommand([]string{"git", "clone", "--depth", "1", remote, tmpCloneDir}); err != nil { + return fmt.Errorf("cloning repo source to temp location: %w", err) + } + + mainVersion, err := semver.GetFromFile(filepath.Join(tmpCloneDir, "VERSION")) + if err != nil { + return err + } + iprint.Debugf("VERSION on main branch: %s\n", version) + + // Need to check if we're already on the main branch, since checking its version against itself + // will unintentionally fail + // + // TODO: update internal git package to have a type with ALL this info so I stop copy-pasting + // shell-outs around + branch, err := tools.RunCommand([]string{"git", "rev-parse", "--abbrev-ref", "HEAD"}) + if err != nil { + return fmt.Errorf("checking current Git branch/ref: %w", err) + } + iprint.Debugf("current Git branch/ref: %s\n", branch) + + if branch != "main" { + if !semver.VersionWasIncremented(version, mainVersion) { + return fmt.Errorf( + "version in 'VERSION' file on this branch (%s) has not been incremented from the version on the main branch", + version, + ) + } + } + + return nil +} + +// Post implements [tools.Tasker.Post]. +func (t versionCI) Post() error { return nil } diff --git a/mise.toml b/mise.toml index 5445be7..f93200f 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,6 @@ [tools] bats = "v1.12.0" +github-cli = "2.78.0" go = "1.25.0" markdownlint-cli2 = "v0.18.1" # node is required for markdownlint-cli2 diff --git a/scripts/xbuild.sh b/scripts/xbuild.sh deleted file mode 100755 index b0df7d3..0000000 --- a/scripts/xbuild.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -targets=$(go tool dist list | grep -E 'linux|darwin' | grep -E 'amd64|arm64') -printf 'Will build for:\n' -while read -r line ; do - printf '\t%s\n' "${line}" -done <<< "${targets}" - -for target in ${targets} ; do - GOOS=$(echo "${target}" | cut -d'/' -f1) - GOARCH=$(echo "${target}" | cut -d'/' -f2) - export GOOS GOARCH - - mkdir -p ./build - out="./build/oscar-${GOOS}-${GOARCH}" - printf "Building to %s\n" "${out}" - go build -o "${out}" ./cmd/oscar - chmod +x "${out}" -done From c0bd02d07d28c3842df922c54ec1a4f8ccbe601a Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Mon, 1 Sep 2025 22:00:56 -0500 Subject: [PATCH 03/38] WIP to introduce oscar config file --- CONTRIBUTING.md | 3 ++ Containerfile | 2 +- README.md | 23 ++++++++--- embed.go | 2 +- go.mod | 1 + go.sum | 2 + internal/cli/root.go | 27 ++++++------- internal/consts/consts.go | 3 ++ internal/git/deliver.go | 31 +++++---------- internal/oscarcfg/config.go | 66 +++++++++++++++++++++++++++++++ internal/oscarcfg/config_test.go | 29 ++++++++++++++ internal/oscarcfg/doc.go | 2 + internal/oscarcfg/test.oscar.yaml | 8 ++++ internal/semver/semver.go | 41 +------------------ internal/tasks/ci/run.go | 10 ++--- internal/tools/version/version.go | 22 ++++++----- oscar.yaml | 6 +++ 17 files changed, 181 insertions(+), 97 deletions(-) create mode 100644 internal/oscarcfg/config.go create mode 100644 internal/oscarcfg/config_test.go create mode 100644 internal/oscarcfg/doc.go create mode 100644 internal/oscarcfg/test.oscar.yaml create mode 100644 oscar.yaml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 559fcff..4348cd5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,3 +20,6 @@ ensure that you have a container runtime available (like Docker or Podman, overr a language or tool you would like to see added, then those contributions are welcome. Fundamental changes to how `oscar` intends to operate, e.g. allowing for wholesale override of various linters' settings, are not. + +* `hEy tHeRe'S a LoT oF ShElL cOdE iN hErE?!` -- Correct. "Shelling out" is an intentional design + decision, and `oscar` calls out `bash` as a dependency. diff --git a/Containerfile b/Containerfile index 6f85d76..c022c91 100644 --- a/Containerfile +++ b/Containerfile @@ -54,7 +54,7 @@ COPY --from=builder /go/app/build/oscar /oscar # NOTE: Docker BuildKit will skip stages it doesn't see as dependencies, so to enforce the "ci" # stage above to run, we need to force a dependency here -COPY --from=ci /go/app/VERSION /VERSION +COPY --from=ci /go/app/oscar.yaml /oscar.yaml RUN apt-get update && apt-get install -y \ bash \ diff --git a/README.md b/README.md index ad8b42d..5433a0c 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ list via `oscar --help`. ## Features -| Feature | `oscar` command | Details | -| :--------------------------- | :-------------- | :--------------------------------- | -| Continuous integration | `oscar ci` | [section](#continuous-integration) | +| Feature | `oscar` command | Details | +| :--------------------- | :-------------- | :--------------------------------- | +| Continuous integration | `oscar ci` | [section](#continuous-integration) | +| Delivery | `oscar deliver` | [section](#delivery) | - ### Continuous Integration @@ -57,6 +57,17 @@ possibly run against within a set of codebases. However, this does not mean that someone is prevented from adding *additional* checks outside of `oscar`'s purview -- it just means that you cannot override what `oscar` *does* control. +### Delivery + +TODO + +| Artifact types | Targets | +| :------------- | :-------------- | +| Go binaries | GitHub Releases | + + + + ## Supported platforms `oscar` is designed to run on Linux, and should work on macOS as well. Native Windows has not been @@ -69,14 +80,14 @@ Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details about developing `os ## Roadmap -* Add `VERSION` check comparing to `main` as a CI task -* Add check for changelog Markdown file that matches what's in `VERSION` (we should also use that +* Add check for changelog Markdown file that matches `oscar.yaml:version` (we should also use that file as the exact GH Release post contents) * Workstation setup * Have `oscar` manage Makefiles, dotfiles, etc. * CI additions * Terraform * protobuf + * R * Rust? * CD additions * Publish to ghcr diff --git a/embed.go b/embed.go index e450632..c020146 100644 --- a/embed.go +++ b/embed.go @@ -8,5 +8,5 @@ import "embed" // 'mise.toml' file that is used for not only oscar's own development config but also for its // internals. // -//go:embed VERSION mise.toml +//go:embed mise.toml oscar.yaml var Files embed.FS diff --git a/go.mod b/go.mod index 1912f83..b840406 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/opensourcecorp/oscar go 1.24.4 require ( + github.com/goccy/go-yaml v1.18.0 github.com/urfave/cli/v3 v3.4.1 golang.org/x/mod v0.27.0 ) diff --git a/go.sum b/go.sum index 8151f88..741856c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= diff --git a/internal/cli/root.go b/internal/cli/root.go index 8f79312..1320420 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -5,10 +5,9 @@ import ( "errors" "fmt" "os" - "strings" - "github.com/opensourcecorp/oscar" "github.com/opensourcecorp/oscar/internal/consts" + "github.com/opensourcecorp/oscar/internal/oscarcfg" iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/tasks/ci" "github.com/opensourcecorp/oscar/internal/tasks/delivery" @@ -27,10 +26,16 @@ const ( // NewRootCmd defines & returns the CLI command used as oscar's entrypoint. func NewRootCmd() *cli.Command { + version, err := getVersion() + if err != nil { + iprint.Errorf("determining your version: %v\n", err) + os.Exit(1) + } + cmd := &cli.Command{ Name: rootCmdName, Usage: "The OpenSourceCorp Automation Runner", - Version: getVersion(), + Version: version, Action: rootAction, Flags: []cli.Flag{ &cli.BoolFlag{ @@ -64,21 +69,13 @@ func maybeSetDebug(cmd *cli.Command) { } // getVersion retrieves the version of the codebase. -func getVersion() string { - contents, err := oscar.Files.ReadFile("VERSION") +func getVersion() (string, error) { + cfg, err := oscarcfg.Get() if err != nil { - panic(fmt.Sprintf("Internal error trying to read VERSION file: %v", err)) + return "", fmt.Errorf("internal error trying to read oscar config file: %w", err) } - splits := strings.Split(string(contents), "\n") - var version string - for _, line := range splits { - if !strings.HasPrefix(line, "#") { - version = line - break - } - } - return version + return cfg.Version, nil } // rootAction defines the logic for oscar's root command. diff --git a/internal/consts/consts.go b/internal/consts/consts.go index feed930..eddad8c 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -14,6 +14,9 @@ const ( // MiseVersion is the default version of mise to install if not present. Can be overridden via // the `MISE_VERSION` env var, which is checked elsewhere. MiseVersion = "v2025.8.21" + + // DefaultOscarCfgFileName is the default basename of oscar's config file. + DefaultOscarCfgFileName = "oscar.yaml" ) var ( diff --git a/internal/git/deliver.go b/internal/git/deliver.go index 7ee08fd..a695025 100644 --- a/internal/git/deliver.go +++ b/internal/git/deliver.go @@ -2,11 +2,8 @@ package git import ( "fmt" - "os" - "path/filepath" - "regexp" - "strings" + "github.com/opensourcecorp/oscar/internal/oscarcfg" iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/tools" ) @@ -15,8 +12,8 @@ import ( type Delivery struct { Root string LatestTag string - // From {Root}/VERSION file - CurrentVersionFromFile string + // From oscar config file + CurrentVersion string } // NewForDelivery returns Git information for Delivery tasks. @@ -31,28 +28,20 @@ func NewForDelivery() (*Delivery, error) { return nil, err } - versionFileContents, err := os.ReadFile(filepath.Join(root, "VERSION")) + cfg, err := oscarcfg.Get() if err != nil { - return nil, err - } - - versionFileLines := strings.Split(string(versionFileContents), "\n") - var version string - for _, line := range versionFileLines { - if regexp.MustCompile(`[0-9]+\.[0-9]+\.[0-9]+`).MatchString(line) { - version = line - break - } + return nil, fmt.Errorf("getting oscar config: %w", err) } + version := cfg.Version if version == "" { - return nil, fmt.Errorf("could not determine a Semantic Version from your 'VERSION' file") + return nil, fmt.Errorf("could not determine a Semantic Version from your oscar config file") } out := &Delivery{ - Root: root, - LatestTag: latestTag, - CurrentVersionFromFile: version, + Root: root, + LatestTag: latestTag, + CurrentVersion: version, } iprint.Debugf("git.Delivery: %+v\n", out) diff --git a/internal/oscarcfg/config.go b/internal/oscarcfg/config.go new file mode 100644 index 0000000..463de9a --- /dev/null +++ b/internal/oscarcfg/config.go @@ -0,0 +1,66 @@ +package oscarcfg + +import ( + "fmt" + "os" + + "github.com/goccy/go-yaml" + "github.com/opensourcecorp/oscar/internal/consts" + iprint "github.com/opensourcecorp/oscar/internal/print" +) + +// Config defines the top-level structure of oscar's config file. +type Config struct { + // Version is the version string for the codebase. + Version string `yaml:"version" json:"version"` + // Deliver is the collection of possible deliverable artifacts. + Deliver Deliverables `yaml:"deliver" json:"deliver"` + // Deploy Deployables `yaml:"deploy" json:"deploy"` +} + +// Deliverables contains a field for each possible deliverable. +type Deliverables struct { + // GoBinaries lists out the Go binaries the user wants to build. + GoBinaries []GoBinary `yaml:"go_binaries" json:"go_binaries"` + GoGitHubRelease GoGitHubRelease `yaml:"go_github_release" json:"go_github_release"` +} + +// GoBinary defines the arguments necessary to build Go binaries. While most other Go-related tasks +// should handle the builds as well, this deliverable type is here to allow users to handle the +// resulting artifacts on their own. +type GoBinary struct { + // BuildSource is the filepath to the "main" package to be built. + BuildSource string `yaml:"build_source" json:"build_source"` +} + +// GoGitHubRelease defines the arguments necessary to create GitHub Releases for Go binaries. +type GoGitHubRelease struct { + Repo string `yaml:"repo" json:"repo"` + // BuildSources are the filepaths to the "main" packages to be built. + BuildSources []string `yaml:"build_sources" json:"build_sources"` +} + +// Get returns a populated [Config] based on the oscar config file location. If `path` is not +// provided, it will default to looking in the calling directory. +func Get(pathOverride ...string) (*Config, error) { + path := consts.DefaultOscarCfgFileName + + // Handle the override so we can test this function, and use it in other ways (like checking the + // main branch's version data) + if len(pathOverride) > 0 { + path = pathOverride[0] + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading oscar config file: %w", err) + } + iprint.Debugf("data read from oscar config file: %s\n", string(data)) + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("unmarshalling oscar config file '%s': %w", path, err) + } + + return &cfg, nil +} diff --git a/internal/oscarcfg/config_test.go b/internal/oscarcfg/config_test.go new file mode 100644 index 0000000..fa06511 --- /dev/null +++ b/internal/oscarcfg/config_test.go @@ -0,0 +1,29 @@ +package oscarcfg + +import ( + "testing" +) + +func TestRead(t *testing.T) { + cfg, err := Get("test.oscar.yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + t.Logf("parsed cfg: %+v", cfg) + + t.Run("version", func(t *testing.T) { + want := "1.0.0" + if cfg.Version != want { + t.Errorf("version: wanted '%s', got '%s'", want, cfg.Version) + } + }) + + t.Run("deliver", func(t *testing.T) { + wantGHRelease := "test" + gotGHRelease := cfg.Deliver.GoGitHubRelease.Repo + if gotGHRelease != wantGHRelease { + t.Errorf("GH Release: wanted '%s', got '%s'", wantGHRelease, gotGHRelease) + } + }) +} diff --git a/internal/oscarcfg/doc.go b/internal/oscarcfg/doc.go new file mode 100644 index 0000000..bb255f0 --- /dev/null +++ b/internal/oscarcfg/doc.go @@ -0,0 +1,2 @@ +// Package oscarcfg defines types & behavior for working with oscar's config file format. +package oscarcfg diff --git a/internal/oscarcfg/test.oscar.yaml b/internal/oscarcfg/test.oscar.yaml new file mode 100644 index 0000000..453df94 --- /dev/null +++ b/internal/oscarcfg/test.oscar.yaml @@ -0,0 +1,8 @@ +version: "1.0.0" +deliver: + go_binaries: + - build_source: "./cmd/test" + go_github_release: + repo: "test" + build_sources: + - "./cmd/test" diff --git a/internal/semver/semver.go b/internal/semver/semver.go index 1faea48..89a3c68 100644 --- a/internal/semver/semver.go +++ b/internal/semver/semver.go @@ -2,13 +2,10 @@ package semver import ( "fmt" - "os" - "path/filepath" "regexp" "strings" iprint "github.com/opensourcecorp/oscar/internal/print" - "github.com/opensourcecorp/oscar/internal/tools" xsemver "golang.org/x/mod/semver" ) @@ -24,7 +21,7 @@ func Get(s string) (string, error) { // just grab the whole MMP with a regex matchList := regexp.MustCompile(`[0-9]+(\.[0-9]+)?(\.[0-9]+)?`).FindStringSubmatch(s) if len(matchList) == 0 { - return "", fmt.Errorf("malformed or unmatchable Semantic Version number from 'VERSION' file (got: '%s')", s) + return "", fmt.Errorf("malformed or unmatchable Semantic Version number (got: '%s')", s) } v := matchList[0] @@ -57,7 +54,7 @@ func Get(s string) (string, error) { } if !xsemver.IsValid(v) { - return "", fmt.Errorf("could not understand the Semantic Version you provided in your 'VERSION' file (got: '%s', converted to: '%s')", s, v) + return "", fmt.Errorf("could not understand the Semantic Version you provided (got: '%s', converted to: '%s')", s, v) } // NOW, we can finally strip off the "v" prefix @@ -66,40 +63,6 @@ func Get(s string) (string, error) { return v, nil } -// GetFromFile reads the contents of the codebase's "VERSION" file, and passes it to [Get]. -func GetFromFile(pathOverride ...string) (string, error) { - root, err := tools.RunCommand([]string{"git", "rev-parse", "--show-toplevel"}) - if err != nil { - return "", err - } - - path := filepath.Join(root, "VERSION") - if len(pathOverride) > 0 { - path = pathOverride[0] - } - - versionFileContents, err := os.ReadFile(path) - if err != nil { - return "", err - } - - versionFileLines := strings.Split(string(versionFileContents), "\n") - var version string - for _, line := range versionFileLines { - if v, err := Get(line); err == nil { - version = v - break - } - iprint.Debugf("error matching line in VERSION file: %v\n", err) - } - - if version == "" { - return "", fmt.Errorf("could not determine a Semantic Version from your 'VERSION' file") - } - - return version, nil -} - // VersionWasIncremented reports whether the newVersion is greater than the oldVersion. func VersionWasIncremented(newVersion string, oldVersion string) bool { compValue := xsemver.Compare("v"+newVersion, "v"+oldVersion) diff --git a/internal/tasks/ci/run.go b/internal/tasks/ci/run.go index 7c509db..a4a7238 100644 --- a/internal/tasks/ci/run.go +++ b/internal/tasks/ci/run.go @@ -27,11 +27,11 @@ func GetCITaskMap() (tools.TaskMap, error) { out := make(tools.TaskMap, 0) for langName, getTasksFunc := range map[string]func(tools.Repo) []tools.Tasker{ - "VERSION file": version.TasksForCI, - "Go": igo.TasksForCI, - "Python": python.Tasks, - "Shell": shell.Tasks, - "Markdown": markdown.Tasks, + "Version": version.TasksForCI, + "Go": igo.TasksForCI, + "Python": python.Tasks, + "Shell": shell.Tasks, + "Markdown": markdown.Tasks, } { tasks := getTasksFunc(repo) if len(tasks) > 0 { diff --git a/internal/tools/version/version.go b/internal/tools/version/version.go index e5f4c2f..b88c0ed 100644 --- a/internal/tools/version/version.go +++ b/internal/tools/version/version.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" + "github.com/opensourcecorp/oscar/internal/consts" + "github.com/opensourcecorp/oscar/internal/oscarcfg" iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/semver" "github.com/opensourcecorp/oscar/internal/tools" @@ -23,17 +25,18 @@ func TasksForCI(_ tools.Repo) []tools.Tasker { } // InfoText implements [tools.Tasker.InfoText]. -func (t versionCI) InfoText() string { return "VERSION file checks" } +func (t versionCI) InfoText() string { return "Versioning checks" } // Run implements [tools.Tasker.Run]. func (t versionCI) Run() (err error) { - version, err := semver.GetFromFile() + cfg, err := oscarcfg.Get() if err != nil { - return err + return fmt.Errorf("getting oscar config: %w", err) } - iprint.Debugf("VERSION: %s\n", version) + version := cfg.Version + iprint.Debugf("provided version: %s\n", version) - // NOTE: we clone the repo in question to a temp location to check the VERSION file contents on + // NOTE: we clone the repo in question to a temp location to check the version on // the main branch, instead of e.g. trying to checkout main. This is for may reasons, not the // least of which being that alternatives would be unreliable in e.g. GitHub Actions CI based on // how it treats PR checkouts et al. A small price to pay for reliability. @@ -56,11 +59,12 @@ func (t versionCI) Run() (err error) { return fmt.Errorf("cloning repo source to temp location: %w", err) } - mainVersion, err := semver.GetFromFile(filepath.Join(tmpCloneDir, "VERSION")) + mainCfg, err := oscarcfg.Get(filepath.Join(tmpCloneDir, consts.DefaultOscarCfgFileName)) if err != nil { - return err + return fmt.Errorf("getting oscar config: %w", err) } - iprint.Debugf("VERSION on main branch: %s\n", version) + mainVersion := mainCfg.Version + iprint.Debugf("main version: %s\n", version) // Need to check if we're already on the main branch, since checking its version against itself // will unintentionally fail @@ -76,7 +80,7 @@ func (t versionCI) Run() (err error) { if branch != "main" { if !semver.VersionWasIncremented(version, mainVersion) { return fmt.Errorf( - "version in 'VERSION' file on this branch (%s) has not been incremented from the version on the main branch", + "version in oscar config on this branch (%s) has not been incremented from the version on the main branch", version, ) } diff --git a/oscar.yaml b/oscar.yaml new file mode 100644 index 0000000..9de2cf9 --- /dev/null +++ b/oscar.yaml @@ -0,0 +1,6 @@ +version: "1.0.0" +deliver: + go_github_release: + repo: "opensourcecorp/oscar" + build_sources: + - "./cmd/oscar" From d64008009cbae1faf58ffb79999a2850966ff235 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Thu, 4 Sep 2025 08:53:13 -0500 Subject: [PATCH 04/38] Add to-dos to the Roadmap --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5433a0c..05eedf4 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details about developing `os file as the exact GH Release post contents) * Workstation setup * Have `oscar` manage Makefiles, dotfiles, etc. + * Also have it dump its own `mise.toml` for the user + * `self-update` subcommand * CI additions * Terraform * protobuf From 9bfcd51d5a6ff2bd3ff866677f61e68987fcc1db Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 7 Sep 2025 20:33:57 -0500 Subject: [PATCH 05/38] WIP --- Makefile | 5 +++++ internal/tools/go/deliver.go | 7 +++++-- mise.toml | 3 +-- scripts/test-bootstrap.sh | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index dca6c99..a0ff4a0 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,11 @@ ci: clean # test is just an alias for ci test: ci +# NOTE: oscar builds itself IRL, but having a target here makes it easier to have the Containerfile +# have a stage-copiable output +build: + @$(RUN) go build -o ./build/oscar ./cmd/oscar + ci-container: @$(DOCKER) build \ --build-arg http_proxy="$${http_proxy}" \ diff --git a/internal/tools/go/deliver.go b/internal/tools/go/deliver.go index f6efebd..4caf5b4 100644 --- a/internal/tools/go/deliver.go +++ b/internal/tools/go/deliver.go @@ -59,8 +59,11 @@ func (t ghRelease) Run() error { target := filepath.Join(targetDir, fmt.Sprintf("%s-%s-%s", binName, goos, goarch)) if _, err := tools.RunCommand([]string{"bash", "-c", fmt.Sprintf(` - GOOS=%s GOARCH=%s go build -o %s %s`, - goos, goarch, target, src, + CGO_ENABLED=0 \ + GOOS=%s GOARCH=%s \ + go build -ldflags '-extldflags "-static"' -o %s %s`, + goos, goarch, + target, src, )}); err != nil { return fmt.Errorf("building Go binary: %w", err) } diff --git a/mise.toml b/mise.toml index f93200f..f5630be 100644 --- a/mise.toml +++ b/mise.toml @@ -2,8 +2,7 @@ bats = "v1.12.0" github-cli = "2.78.0" go = "1.25.0" -markdownlint-cli2 = "v0.18.1" -# node is required for markdownlint-cli2 +markdownlint-cli2 = "v0.18.1" # required for markdownlint-cli2 node = "24.7.0" shellcheck = "v0.11.0" shfmt = "v3.12.0" diff --git a/scripts/test-bootstrap.sh b/scripts/test-bootstrap.sh index a57b209..5eed9b8 100644 --- a/scripts/test-bootstrap.sh +++ b/scripts/test-bootstrap.sh @@ -3,7 +3,7 @@ set -euo pipefail shopt -s globstar _setup() { - cp ./internal/ci/configfiles/pyproject.toml . + cp ./internal/tools/toolcfg/pyproject.toml . mkdir -p ./src cp -r ./testdata/python/src ./src/test_package rename 's/\.test//g' -- ./src/** From 28e4f9d9420047cb97008bc87f083713d8a75444 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Mon, 8 Sep 2025 23:30:19 -0500 Subject: [PATCH 06/38] big ol' wipper to add YAML CI and refactor to add ctx --- .github/workflows/main.yaml | 75 +++++++------ docker-compose.yaml | 1 + internal/cli/root.go | 8 +- internal/git/ci.go | 21 ++-- internal/git/deliver.go | 7 +- internal/oscarcfg/test.oscar.yaml | 1 + internal/tasks/ci/run.go | 45 ++++---- internal/tasks/delivery/run.go | 19 ++-- internal/tools/go/ci.go | 102 +++++++----------- internal/tools/go/deliver.go | 9 +- internal/tools/go/doc.go | 4 +- internal/tools/markdown/doc.go | 4 +- internal/tools/markdown/tasks.go | 9 +- internal/tools/markdown/versions.go | 2 +- internal/tools/python/doc.go | 4 +- internal/tools/python/tasks.go | 41 +++---- internal/tools/python/versions.go | 2 +- internal/tools/shell/doc.go | 4 +- internal/tools/shell/tasks.go | 39 ++----- internal/tools/shell/versions.go | 8 +- .../tools/toolcfg/.markdownlint-cli2.yaml | 1 + internal/tools/toolcfg/.yamlfmt | 5 + internal/tools/toolcfg/.yamllint | 15 +++ internal/tools/toolcfg/embed.go | 2 +- internal/tools/types.go | 32 ++++-- internal/tools/util.go | 57 ++++++---- internal/tools/version/doc.go | 4 +- internal/tools/version/version.go | 13 +-- internal/tools/yaml/doc.go | 2 + internal/tools/yaml/tasks.go | 92 ++++++++++++++++ internal/tools/yaml/versions.go | 19 ++++ mise.toml | 26 +++-- oscar.yaml | 1 + 33 files changed, 403 insertions(+), 271 deletions(-) create mode 100644 internal/tools/toolcfg/.yamlfmt create mode 100644 internal/tools/toolcfg/.yamllint create mode 100644 internal/tools/yaml/doc.go create mode 100644 internal/tools/yaml/tasks.go create mode 100644 internal/tools/yaml/versions.go diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 43fa827..b9b0dce 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,5 +1,4 @@ name: main - on: push: branches: @@ -9,14 +8,12 @@ on: - opened - reopened - synchronize - jobs: ci: runs-on: ubuntu-latest permissions: contents: read packages: write - steps: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # release v5.0.0 @@ -34,43 +31,43 @@ jobs: - name: Run CI run: make ci - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v2 +# - name: Set up Docker Buildx +# uses: docker/setup-buildx-action@v2 - # - name: Log in to GHCR - # uses: docker/login-action@v2 - # with: - # registry: ghcr.io - # username: ${{ github.repository_owner }} - # password: ${{ secrets.GITHUB_TOKEN }} +# - name: Log in to GHCR +# uses: docker/login-action@v2 +# with: +# registry: ghcr.io +# username: ${{ github.repository_owner }} +# password: ${{ secrets.GITHUB_TOKEN }} - # # Generate any tags we want for the images (https://github.com/docker/build-push-action/blob/master/docs/advanced/tags-labels.md) - # - name: Extract metadata (tags, labels) for OCI image - # id: meta - # uses: docker/metadata-action@v4 - # with: - # images: ghcr.io/opensourcecorp/oscar - # tags: | - # type=sha +# # Generate any tags we want for the images (https://github.com/docker/build-push-action/blob/master/docs/advanced/tags-labels.md) +# - name: Extract metadata (tags, labels) for OCI image +# id: meta +# uses: docker/metadata-action@v4 +# with: +# images: ghcr.io/opensourcecorp/oscar +# tags: | +# type=sha - # # non-mainline - # - name: Build and push non-mainline OCI image - # if: github.ref != 'refs/heads/main' - # uses: docker/build-push-action@v3 - # with: - # context: . - # file: Containerfile - # push: true - # tags: - # - "latest" - # - "${VERSION}" +# # non-mainline +# - name: Build and push non-mainline OCI image +# if: github.ref != 'refs/heads/main' +# uses: docker/build-push-action@v3 +# with: +# context: . +# file: Containerfile +# push: true +# tags: +# - "latest" +# - "${VERSION}" - # # mainline - # - name: Build and push mainline OCI image - # if: github.ref == 'refs/heads/main' - # uses: docker/build-push-action@v3 - # with: - # context: . - # file: Containerfile - # push: true - # tags: ghcr.io/opensourcecorp/oscar:latest +# # mainline +# - name: Build and push mainline OCI image +# if: github.ref == 'refs/heads/main' +# uses: docker/build-push-action@v3 +# with: +# context: . +# file: Containerfile +# push: true +# tags: ghcr.io/opensourcecorp/oscar:latest diff --git a/docker-compose.yaml b/docker-compose.yaml index 7dc09cc..bed77fd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,3 +1,4 @@ +--- services: oscar: container_name: "oscar" diff --git a/internal/cli/root.go b/internal/cli/root.go index 1320420..ff427fd 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -87,12 +87,12 @@ func rootAction(_ context.Context, cmd *cli.Command) error { } // ciAction defines the logic for oscar's ci subcommand. -func ciAction(_ context.Context, cmd *cli.Command) error { +func ciAction(ctx context.Context, cmd *cli.Command) error { maybeSetDebug(cmd) iprint.Banner() iprint.Debugf("oscar ci subcommand\n") - if err := ci.Run(); err != nil { + if err := ci.Run(ctx); err != nil { return fmt.Errorf("running CI tasks: %w", err) } @@ -100,12 +100,12 @@ func ciAction(_ context.Context, cmd *cli.Command) error { } // deliverAction defines the logic for oscar's deliver subcommand. -func deliverAction(_ context.Context, cmd *cli.Command) error { +func deliverAction(ctx context.Context, cmd *cli.Command) error { maybeSetDebug(cmd) iprint.Banner() iprint.Debugf("oscar deliver subcommand\n") - if err := delivery.Run(); err != nil { + if err := delivery.Run(ctx); err != nil { return fmt.Errorf("running Delivery tasks: %w", err) } diff --git a/internal/git/ci.go b/internal/git/ci.go index 02d64a9..ecfd6bc 100644 --- a/internal/git/ci.go +++ b/internal/git/ci.go @@ -1,6 +1,7 @@ package git import ( + "context" "fmt" "regexp" "slices" @@ -26,8 +27,8 @@ type Status struct { } // NewForCI returns Git information for CI tasks. -func NewForCI() (*CI, error) { - status, err := getRawStatus() +func NewForCI(ctx context.Context) (*CI, error) { + status, err := getRawStatus(ctx) if err != nil { return nil, err } @@ -38,8 +39,8 @@ func NewForCI() (*CI, error) { } // Update recalculates various Git metadata, respecting any existing baseline values set in [NewForCI]. -func (g *CI) Update() error { - status, err := getRawStatus() +func (g *CI) Update(ctx context.Context) error { + status, err := getRawStatus(ctx) if err != nil { return fmt.Errorf("getting Git status: %w", err) } @@ -70,8 +71,8 @@ func (g *CI) Update() error { } // StatusHasChanged informs the caller of whether or not the [Status] now differs from the baseline. -func (g *CI) StatusHasChanged() (bool, error) { - if err := g.updateStatus(); err != nil { +func (g *CI) StatusHasChanged(ctx context.Context) (bool, error) { + if err := g.updateStatus(ctx); err != nil { return false, err } @@ -90,8 +91,8 @@ func (g *CI) StatusHasChanged() (bool, error) { // getRawStatus returns a slightly-modified "git status" output, so that calling tools can parse it // more easily. -func getRawStatus() (Status, error) { - outputBytes, err := tools.RunCommand([]string{"git", "status", "--porcelain"}) +func getRawStatus(ctx context.Context) (Status, error) { + outputBytes, err := tools.RunCommand(ctx, []string{"git", "status", "--porcelain"}) if err != nil { return Status{}, fmt.Errorf("getting git status output: %w", err) } @@ -121,13 +122,13 @@ func getRawStatus() (Status, error) { } // updateStatus updates the tracked Git status so that it can be compared against the baseline. -func (g *CI) updateStatus() error { +func (g *CI) updateStatus(ctx context.Context) error { // So any future debug logs have a line break in them iprint.Debugf("\n") iprint.Debugf("OLD git: %+v\n", g) - status, err := getRawStatus() + status, err := getRawStatus(ctx) if err != nil { return fmt.Errorf("getting Git status: %w", err) } diff --git a/internal/git/deliver.go b/internal/git/deliver.go index a695025..ea60031 100644 --- a/internal/git/deliver.go +++ b/internal/git/deliver.go @@ -1,6 +1,7 @@ package git import ( + "context" "fmt" "github.com/opensourcecorp/oscar/internal/oscarcfg" @@ -17,13 +18,13 @@ type Delivery struct { } // NewForDelivery returns Git information for Delivery tasks. -func NewForDelivery() (*Delivery, error) { - root, err := tools.RunCommand([]string{"git", "rev-parse", "--show-toplevel"}) +func NewForDelivery(ctx context.Context) (*Delivery, error) { + root, err := tools.RunCommand(ctx, []string{"git", "rev-parse", "--show-toplevel"}) if err != nil { return nil, err } - latestTag, err := tools.RunCommand([]string{"bash", "-c", "git tag --list | tail -n1"}) + latestTag, err := tools.RunCommand(ctx, []string{"bash", "-c", "git tag --list | tail -n1"}) if err != nil { return nil, err } diff --git a/internal/oscarcfg/test.oscar.yaml b/internal/oscarcfg/test.oscar.yaml index 453df94..00d06fd 100644 --- a/internal/oscarcfg/test.oscar.yaml +++ b/internal/oscarcfg/test.oscar.yaml @@ -1,3 +1,4 @@ +--- version: "1.0.0" deliver: go_binaries: diff --git a/internal/tasks/ci/run.go b/internal/tasks/ci/run.go index a4a7238..93cf4f3 100644 --- a/internal/tasks/ci/run.go +++ b/internal/tasks/ci/run.go @@ -1,6 +1,7 @@ package ci import ( + "context" "errors" "fmt" "os" @@ -11,27 +12,29 @@ import ( "github.com/opensourcecorp/oscar/internal/git" iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/tools" - igo "github.com/opensourcecorp/oscar/internal/tools/go" - "github.com/opensourcecorp/oscar/internal/tools/markdown" - "github.com/opensourcecorp/oscar/internal/tools/python" - "github.com/opensourcecorp/oscar/internal/tools/shell" - "github.com/opensourcecorp/oscar/internal/tools/version" + gotools "github.com/opensourcecorp/oscar/internal/tools/go" + mdtools "github.com/opensourcecorp/oscar/internal/tools/markdown" + pytools "github.com/opensourcecorp/oscar/internal/tools/python" + shtools "github.com/opensourcecorp/oscar/internal/tools/shell" + versiontools "github.com/opensourcecorp/oscar/internal/tools/version" + yamltools "github.com/opensourcecorp/oscar/internal/tools/yaml" ) // GetCITaskMap assembles the overall list of CI tasks, keyed by their language/tooling name -func GetCITaskMap() (tools.TaskMap, error) { - repo, err := tools.GetRepoComposition() +func GetCITaskMap(ctx context.Context) (tools.TaskMap, error) { + repo, err := tools.GetRepoComposition(ctx) if err != nil { return nil, fmt.Errorf("getting repo composition: %w", err) } out := make(tools.TaskMap, 0) for langName, getTasksFunc := range map[string]func(tools.Repo) []tools.Tasker{ - "Version": version.TasksForCI, - "Go": igo.TasksForCI, - "Python": python.Tasks, - "Shell": shell.Tasks, - "Markdown": markdown.Tasks, + "Version": versiontools.TasksForCI, + "Go": gotools.TasksForCI, + "Python": pytools.Tasks, + "YAML": yamltools.Tasks, + "Shell": shtools.Tasks, + "Markdown": mdtools.Tasks, } { tasks := getTasksFunc(repo) if len(tasks) > 0 { @@ -48,11 +51,11 @@ func GetCITaskMap() (tools.TaskMap, error) { } // Run defines the behavior for running all CI tasks for the repository. -func Run() (err error) { +func Run(ctx context.Context) (err error) { runStartTime := time.Now() // Handle system init - if err := tools.InitSystem(); err != nil { + if err := tools.InitSystem(ctx); err != nil { return fmt.Errorf("initializing system: %w", err) } // The mise config that oscar uses is written during init, so be sure to defer its removal here @@ -70,7 +73,7 @@ func Run() (err error) { // All the CI tasks that will be looped over. Will also print a summary of discovered file // types. - ciTaskMap, err := GetCITaskMap() + ciTaskMap, err := GetCITaskMap(ctx) if err != nil { return fmt.Errorf("getting CI tasks: %w", err) } @@ -87,7 +90,7 @@ func Run() (err error) { iprint.Debugf("longestInfoTextLength: %d\n", longestInfoTextLength) // For tracking any changes to Git status etc. after each Task runs - gitCI, err := git.NewForCI() + gitCI, err := git.NewForCI(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } @@ -117,13 +120,13 @@ func Run() (err error) { // NOTE: this error is checked later, when we can check the Run, Post, and git-diff // potential errors together var runErr error - runErr = errors.Join(runErr, t.Run()) - runErr = errors.Join(runErr, t.Post()) + runErr = errors.Join(runErr, t.Run(ctx)) + runErr = errors.Join(runErr, t.Post(ctx)) - if err := gitCI.Update(); err != nil { + if err := gitCI.Update(ctx); err != nil { return fmt.Errorf("internal error: %w", err) } - gitStatusHasChanged, err := gitCI.StatusHasChanged() + gitStatusHasChanged, err := gitCI.StatusHasChanged(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } @@ -145,7 +148,7 @@ func Run() (err error) { failures = append(failures, fmt.Sprintf("%s :: %s", lang, t.InfoText())) // Also need to reset the baseline status - gitCI, err = git.NewForCI() + gitCI, err = git.NewForCI(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } diff --git a/internal/tasks/delivery/run.go b/internal/tasks/delivery/run.go index 41d8d63..a6c1a0d 100644 --- a/internal/tasks/delivery/run.go +++ b/internal/tasks/delivery/run.go @@ -1,6 +1,7 @@ package delivery import ( + "context" "errors" "fmt" "os" @@ -10,20 +11,20 @@ import ( "github.com/opensourcecorp/oscar/internal/consts" iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/tools" - igo "github.com/opensourcecorp/oscar/internal/tools/go" + gotools "github.com/opensourcecorp/oscar/internal/tools/go" ) // GetDeliveryTaskMap assembles the overall list of Delivery tasks, keyed by their language/tooling // name. -func GetDeliveryTaskMap() (tools.TaskMap, error) { - repo, err := tools.GetRepoComposition() +func GetDeliveryTaskMap(ctx context.Context) (tools.TaskMap, error) { + repo, err := tools.GetRepoComposition(ctx) if err != nil { return nil, fmt.Errorf("getting repo composition: %w", err) } out := make(tools.TaskMap, 0) for langName, getTasksFunc := range map[string]func(tools.Repo) []tools.Tasker{ - "Go": igo.TasksForDelivery, + "Go": gotools.TasksForDelivery, } { tasks := getTasksFunc(repo) if len(tasks) > 0 { @@ -40,11 +41,11 @@ func GetDeliveryTaskMap() (tools.TaskMap, error) { } // Run defines the behavior for running all Delivery tasks for the repository. -func Run() (err error) { +func Run(ctx context.Context) (err error) { runStartTime := time.Now() // Handle system init - if err := tools.InitSystem(); err != nil { + if err := tools.InitSystem(ctx); err != nil { return fmt.Errorf("initializing system: %w", err) } // The mise config that oscar uses is written during init, so be sure to defer its removal here @@ -62,7 +63,7 @@ func Run() (err error) { // All the Delivery tasks that will be looped over. Will also print a summary of discovered file // types. - deliveryTaskMap, err := GetDeliveryTaskMap() + deliveryTaskMap, err := GetDeliveryTaskMap(ctx) if err != nil { return fmt.Errorf("getting Delivery tasks: %w", err) } @@ -103,8 +104,8 @@ func Run() (err error) { // NOTE: this error is checked later, when we can check the Run, Post, and git-diff // potential errors together var runErr error - runErr = errors.Join(runErr, t.Run()) - runErr = errors.Join(runErr, t.Post()) + runErr = errors.Join(runErr, t.Run(ctx)) + runErr = errors.Join(runErr, t.Post(ctx)) if runErr != nil { iprint.Errorf("FAILED (%s)\n", tools.RunDurationString(taskStartTime)) diff --git a/internal/tools/go/ci.go b/internal/tools/go/ci.go index cda561d..bb92f4d 100644 --- a/internal/tools/go/ci.go +++ b/internal/tools/go/ci.go @@ -1,10 +1,10 @@ -package igo +package gotools import ( + "context" "fmt" "os" "path/filepath" - "slices" "github.com/opensourcecorp/oscar/internal/tools" "github.com/opensourcecorp/oscar/internal/tools/toolcfg" @@ -41,30 +41,20 @@ var ciTasks = []tools.Tasker{ var ( staticcheck = tools.Tool{ Name: "staticcheck", - RemotePath: "honnef.co/go/tools/cmd/staticcheck", - Version: "2025.1.1", ConfigFilePath: filepath.Join("./staticcheck.conf"), } revive = tools.Tool{ Name: "revive", - RemotePath: "github.com/mgechev/revive", - Version: "v1.11.0", ConfigFilePath: filepath.Join(os.TempDir(), "revive.toml"), } errcheck = tools.Tool{ - Name: "errcheck", - RemotePath: "github.com/kisielk/errcheck", - Version: "v1.9.0", + Name: "errcheck", } goimports = tools.Tool{ - Name: "goimports", - RemotePath: "golang.org/x/tools/cmd/goimports", - Version: "v0.35.0", + Name: "goimports", } govulncheck = tools.Tool{ - Name: "govulncheck", - RemotePath: "golang.org/x/vuln/cmd/govulncheck", - Version: "v1.1.4", + Name: "govulncheck", } ) @@ -81,8 +71,8 @@ func TasksForCI(repo tools.Repo) []tools.Tasker { func (t goModCheckCI) InfoText() string { return "go.mod tidy check" } // Run implements [tools.Tasker.Run]. -func (t goModCheckCI) Run() error { - if _, err := tools.RunCommand([]string{"go", "mod", "tidy"}); err != nil { +func (t goModCheckCI) Run(ctx context.Context) error { + if _, err := tools.RunCommand(ctx, []string{"go", "mod", "tidy"}); err != nil { return err } @@ -90,14 +80,14 @@ func (t goModCheckCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goModCheckCI) Post() error { return nil } +func (t goModCheckCI) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t goFormatCI) InfoText() string { return "Format" } // Run implements [tools.Tasker.Run]. -func (t goFormatCI) Run() error { - if _, err := tools.RunCommand([]string{"go", "fmt", "./..."}); err != nil { +func (t goFormatCI) Run(ctx context.Context) error { + if _, err := tools.RunCommand(ctx, []string{"go", "fmt", "./..."}); err != nil { return err } @@ -105,14 +95,14 @@ func (t goFormatCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goFormatCI) Post() error { return nil } +func (t goFormatCI) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t generateCodeCI) InfoText() string { return "Generate code" } // Run implements [tools.Tasker.Run]. -func (t generateCodeCI) Run() error { - if _, err := tools.RunCommand([]string{"go", "generate", "./..."}); err != nil { +func (t generateCodeCI) Run(ctx context.Context) error { + if _, err := tools.RunCommand(ctx, []string{"go", "generate", "./..."}); err != nil { return err } @@ -120,14 +110,14 @@ func (t generateCodeCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t generateCodeCI) Post() error { return nil } +func (t generateCodeCI) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t goBuildCI) InfoText() string { return "Build" } // Run implements [tools.Tasker.Run]. -func (t goBuildCI) Run() error { - if _, err := tools.RunCommand([]string{"go", "build", "./..."}); err != nil { +func (t goBuildCI) Run(ctx context.Context) error { + if _, err := tools.RunCommand(ctx, []string{"go", "build", "./..."}); err != nil { return err } @@ -135,14 +125,14 @@ func (t goBuildCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goBuildCI) Post() error { return nil } +func (t goBuildCI) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t goVetCI) InfoText() string { return "Vet" } // Run implements [tools.Tasker.Run]. -func (t goVetCI) Run() error { - if _, err := tools.RunCommand([]string{"go", "vet", "./..."}); err != nil { +func (t goVetCI) Run(ctx context.Context) error { + if _, err := tools.RunCommand(ctx, []string{"go", "vet", "./..."}); err != nil { return err } @@ -150,13 +140,13 @@ func (t goVetCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goVetCI) Post() error { return nil } +func (t goVetCI) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t staticcheckCI) InfoText() string { return "Lint (staticcheck)" } // Run implements [tools.Tasker.Run]. -func (t staticcheckCI) Run() (err error) { +func (t staticcheckCI) Run(ctx context.Context) (err error) { cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(staticcheck.ConfigFilePath)) if err != nil { return fmt.Errorf("reading embedded file contents: %w", err) @@ -166,7 +156,7 @@ func (t staticcheckCI) Run() (err error) { return fmt.Errorf("writing config file: %w", err) } - if err := goRun(staticcheck, "./..."); err != nil { + if _, err := tools.RunCommand(ctx, []string{staticcheck.Name, "./..."}); err != nil { return err } @@ -174,7 +164,7 @@ func (t staticcheckCI) Run() (err error) { } // Post implements [tools.Tasker.Post]. -func (t staticcheckCI) Post() error { +func (t staticcheckCI) Post(_ context.Context) error { if err := os.RemoveAll(staticcheck.ConfigFilePath); err != nil { return fmt.Errorf("removing config file: %w", err) } @@ -186,7 +176,7 @@ func (t staticcheckCI) Post() error { func (t reviveCI) InfoText() string { return "Lint (revive)" } // Run implements [tools.Tasker.Run]. -func (t reviveCI) Run() error { +func (t reviveCI) Run(ctx context.Context) error { cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(revive.ConfigFilePath)) if err != nil { return fmt.Errorf("reading embedded file contents: %w", err) @@ -197,12 +187,13 @@ func (t reviveCI) Run() error { } args := []string{ + revive.Name, "--config", revive.ConfigFilePath, "--set_exit_status", "./...", } - if err := goRun(revive, args...); err != nil { + if _, err := tools.RunCommand(ctx, args); err != nil { return err } @@ -210,7 +201,7 @@ func (t reviveCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t reviveCI) Post() error { +func (t reviveCI) Post(_ context.Context) error { if err := os.RemoveAll(revive.ConfigFilePath); err != nil { return fmt.Errorf("removing config file: %w", err) } @@ -222,8 +213,8 @@ func (t reviveCI) Post() error { func (t errcheckCI) InfoText() string { return "Lint (errcheck)" } // Run implements [tools.Tasker.Run]. -func (t errcheckCI) Run() error { - if err := goRun(errcheck, "./..."); err != nil { +func (t errcheckCI) Run(ctx context.Context) error { + if _, err := tools.RunCommand(ctx, []string{errcheck.Name, "./..."}); err != nil { return err } @@ -231,15 +222,15 @@ func (t errcheckCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t errcheckCI) Post() error { return nil } +func (t errcheckCI) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t goImportsCI) InfoText() string { return "Format imports" } // Run implements [tools.Tasker.Run]. -func (t goImportsCI) Run() error { - args := []string{"-l", "-w", "."} - if err := goRun(goimports, args...); err != nil { +func (t goImportsCI) Run(ctx context.Context) error { + args := []string{goimports.Name, "-l", "-w", "."} + if _, err := tools.RunCommand(ctx, args); err != nil { return err } @@ -247,14 +238,14 @@ func (t goImportsCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goImportsCI) Post() error { return nil } +func (t goImportsCI) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t govulncheckCI) InfoText() string { return "Vulnerability scan (govulncheck)" } // Run implements [tools.Tasker.Run]. -func (t govulncheckCI) Run() error { - if err := goRun(govulncheck, "./..."); err != nil { +func (t govulncheckCI) Run(ctx context.Context) error { + if _, err := tools.RunCommand(ctx, []string{govulncheck.Name, "./..."}); err != nil { return err } @@ -262,14 +253,14 @@ func (t govulncheckCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t govulncheckCI) Post() error { return nil } +func (t govulncheckCI) Post(ctx context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t goTestCI) InfoText() string { return "Test" } // Run implements [tools.Tasker.Run]. -func (t goTestCI) Run() error { - if _, err := tools.RunCommand([]string{"go", "test", "./..."}); err != nil { +func (t goTestCI) Run(ctx context.Context) error { + if _, err := tools.RunCommand(ctx, []string{"go", "test", "./..."}); err != nil { return err } @@ -277,17 +268,4 @@ func (t goTestCI) Run() error { } // Post implements [tools.Tasker.Post]. -func (t goTestCI) Post() error { return nil } - -// goRun is a wrapper for "go run" -func goRun(t tools.Tool, trailingArgs ...string) error { - args := slices.Concat( - []string{"go", "run", fmt.Sprintf("%s@%s", t.RemotePath, t.Version)}, - trailingArgs, - ) - if _, err := tools.RunCommand(args); err != nil { - return fmt.Errorf("running 'go run': %w", err) - } - - return nil -} +func (t goTestCI) Post(ctx context.Context) error { return nil } diff --git a/internal/tools/go/deliver.go b/internal/tools/go/deliver.go index 4caf5b4..28c44ea 100644 --- a/internal/tools/go/deliver.go +++ b/internal/tools/go/deliver.go @@ -1,6 +1,7 @@ -package igo +package gotools import ( + "context" "fmt" "os" "path/filepath" @@ -30,7 +31,7 @@ func TasksForDelivery(repo tools.Repo) []tools.Tasker { func (t ghRelease) InfoText() string { return "GitHub Release" } // Run implements [tools.Tasker.Run]. -func (t ghRelease) Run() error { +func (t ghRelease) Run(ctx context.Context) error { targetDir := "build" if err := os.RemoveAll(targetDir); err != nil { @@ -58,7 +59,7 @@ func (t ghRelease) Run() error { src := "./cmd/oscar" target := filepath.Join(targetDir, fmt.Sprintf("%s-%s-%s", binName, goos, goarch)) - if _, err := tools.RunCommand([]string{"bash", "-c", fmt.Sprintf(` + if _, err := tools.RunCommand(ctx, []string{"bash", "-c", fmt.Sprintf(` CGO_ENABLED=0 \ GOOS=%s GOARCH=%s \ go build -ldflags '-extldflags "-static"' -o %s %s`, @@ -77,4 +78,4 @@ func (t ghRelease) Run() error { } // Post implements [tools.Tasker.Post]. -func (t ghRelease) Post() error { return nil } +func (t ghRelease) Post(_ context.Context) error { return nil } diff --git a/internal/tools/go/doc.go b/internal/tools/go/doc.go index fa65bcd..9efa156 100644 --- a/internal/tools/go/doc.go +++ b/internal/tools/go/doc.go @@ -1,2 +1,2 @@ -// Package igo contains logic for running tasks for Go. -package igo +// Package gotools contains logic for running tasks for Go. +package gotools diff --git a/internal/tools/markdown/doc.go b/internal/tools/markdown/doc.go index 3a6c622..0fe2c56 100644 --- a/internal/tools/markdown/doc.go +++ b/internal/tools/markdown/doc.go @@ -1,2 +1,2 @@ -// Package markdown contains logic for running tasks for Markdown. -package markdown +// Package mdtools contains logic for running tasks for Markdown. +package mdtools diff --git a/internal/tools/markdown/tasks.go b/internal/tools/markdown/tasks.go index 8c4e541..fbe9d20 100644 --- a/internal/tools/markdown/tasks.go +++ b/internal/tools/markdown/tasks.go @@ -1,6 +1,7 @@ -package markdown +package mdtools import ( + "context" "fmt" "os" "path/filepath" @@ -30,7 +31,7 @@ func Tasks(repo tools.Repo) []tools.Tasker { func (t markdownlintTask) InfoText() string { return "Lint (markdownlint)" } // Run implements [tools.Tasker.Run]. -func (t markdownlintTask) Run() error { +func (t markdownlintTask) Run(ctx context.Context) error { cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(markdownlint.ConfigFilePath)) if err != nil { return fmt.Errorf("reading embedded file contents: %w", err) @@ -46,7 +47,7 @@ func (t markdownlintTask) Run() error { "**/*.md", } - if _, err := tools.RunCommand(args); err != nil { + if _, err := tools.RunCommand(ctx, args); err != nil { return err } @@ -54,4 +55,4 @@ func (t markdownlintTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t markdownlintTask) Post() error { return nil } +func (t markdownlintTask) Post(_ context.Context) error { return nil } diff --git a/internal/tools/markdown/versions.go b/internal/tools/markdown/versions.go index f4de820..9f83cbc 100644 --- a/internal/tools/markdown/versions.go +++ b/internal/tools/markdown/versions.go @@ -1,4 +1,4 @@ -package markdown +package mdtools import ( "os" diff --git a/internal/tools/python/doc.go b/internal/tools/python/doc.go index 2b41654..38caea9 100644 --- a/internal/tools/python/doc.go +++ b/internal/tools/python/doc.go @@ -1,2 +1,2 @@ -// Package python contains logic for running tasks for Python. -package python +// Package pytools contains logic for running tasks for Python. +package pytools diff --git a/internal/tools/python/tasks.go b/internal/tools/python/tasks.go index 7086dab..046a19e 100644 --- a/internal/tools/python/tasks.go +++ b/internal/tools/python/tasks.go @@ -1,6 +1,7 @@ -package python +package pytools import ( + "context" "fmt" "slices" @@ -38,21 +39,21 @@ func Tasks(repo tools.Repo) []tools.Tasker { func (t baseConfigTask) InfoText() string { return "" } // Run implements [tools.Tasker.Run]. -func (t baseConfigTask) Run() error { +func (t baseConfigTask) Run(ctx context.Context) error { // ciutil.PlaceConfigFile("pyproject.toml") return nil } // Post implements [tools.Tasker.Post]. -func (t baseConfigTask) Post() error { return nil } +func (t baseConfigTask) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t buildTask) InfoText() string { return "Build" } // Run implements [tools.Tasker.Run]. -func (t buildTask) Run() error { - if _, err := tools.RunCommand([]string{"uv", "build"}); err != nil { +func (t buildTask) Run(ctx context.Context) error { + if _, err := tools.RunCommand(ctx, []string{"uv", "build"}); err != nil { return err } @@ -60,14 +61,14 @@ func (t buildTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t buildTask) Post() error { return nil } +func (t buildTask) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t ruffLintTask) InfoText() string { return "Lint (ruff)" } // Run implements [tools.Tasker.Run]. -func (t ruffLintTask) Run() error { - if err := pyRun(ruffLint, "check", "--fix", "./src"); err != nil { +func (t ruffLintTask) Run(ctx context.Context) error { + if err := pyRun(ctx, ruffLint, "check", "--fix", "./src"); err != nil { return err } @@ -75,28 +76,28 @@ func (t ruffLintTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t ruffLintTask) Post() error { return nil } +func (t ruffLintTask) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t ruffFormatTask) InfoText() string { return "Format (ruff)" } // Run implements [tools.Tasker.Run]. -func (t ruffFormatTask) Run() error { - if err := pyRun(ruffFormat, "format", "./src"); err != nil { +func (t ruffFormatTask) Run(ctx context.Context) error { + if err := pyRun(ctx, ruffFormat, "format", "./src"); err != nil { return err } return nil } // Post implements [tools.Tasker.Post]. -func (t ruffFormatTask) Post() error { return nil } +func (t ruffFormatTask) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t pydoclintTask) InfoText() string { return "Lint (pydoclint)" } // Run implements [tools.Tasker.Run]. -func (t pydoclintTask) Run() error { - if err := pyRun(pydoclint, "./src"); err != nil { +func (t pydoclintTask) Run(ctx context.Context) error { + if err := pyRun(ctx, pydoclint, "./src"); err != nil { return err } @@ -104,14 +105,14 @@ func (t pydoclintTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t pydoclintTask) Post() error { return nil } +func (t pydoclintTask) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t mypyTask) InfoText() string { return "Type-check (mypy)" } // Run implements [tools.Tasker.Run]. -func (t mypyTask) Run() error { - if err := pyRun(mypy, "./src"); err != nil { +func (t mypyTask) Run(ctx context.Context) error { + if err := pyRun(ctx, mypy, "./src"); err != nil { return err } @@ -119,15 +120,15 @@ func (t mypyTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t mypyTask) Post() error { return nil } +func (t mypyTask) Post(_ context.Context) error { return nil } // pyRun is a wrapper for "uvx" -func pyRun(t tools.Tool, trailingArgs ...string) error { +func pyRun(ctx context.Context, t tools.Tool, trailingArgs ...string) error { args := slices.Concat( []string{"uvx", fmt.Sprintf("%s@%s", t.Name, t.Version)}, trailingArgs, ) - if _, err := tools.RunCommand(args); err != nil { + if _, err := tools.RunCommand(ctx, args); err != nil { return fmt.Errorf("running 'uvx': %w", err) } diff --git a/internal/tools/python/versions.go b/internal/tools/python/versions.go index a668c0f..9069758 100644 --- a/internal/tools/python/versions.go +++ b/internal/tools/python/versions.go @@ -1,4 +1,4 @@ -package python +package pytools import ( "github.com/opensourcecorp/oscar/internal/tools" diff --git a/internal/tools/shell/doc.go b/internal/tools/shell/doc.go index fc6a5a9..be333a6 100644 --- a/internal/tools/shell/doc.go +++ b/internal/tools/shell/doc.go @@ -1,2 +1,2 @@ -// Package shell contains logic for running tasks for Shell languages. -package shell +// Package shtools contains logic for running tasks for Shell languages. +package shtools diff --git a/internal/tools/shell/tasks.go b/internal/tools/shell/tasks.go index 8365ad1..8aab5b0 100644 --- a/internal/tools/shell/tasks.go +++ b/internal/tools/shell/tasks.go @@ -1,6 +1,7 @@ -package shell +package shtools import ( + "context" "fmt" "github.com/opensourcecorp/oscar/internal/tools" @@ -9,13 +10,11 @@ import ( type ( shellcheckTask struct{} shfmtTask struct{} - batsTask struct{} ) var tasks = []tools.Tasker{ shellcheckTask{}, shfmtTask{}, - batsTask{}, } // Tasks returns the list of CI tasks. @@ -31,14 +30,14 @@ func Tasks(repo tools.Repo) []tools.Tasker { func (t shellcheckTask) InfoText() string { return "Lint (shellcheck)" } // Run implements [tools.Tasker.Run]. -func (t shellcheckTask) Run() error { +func (t shellcheckTask) Run(ctx context.Context) error { args := []string{"bash", "-c", fmt.Sprintf(` shopt -s globstar ls **/*.sh || exit 0 %s **/*.sh`, shellcheck.Name, )} - if _, err := tools.RunCommand(args); err != nil { + if _, err := tools.RunCommand(ctx, args); err != nil { return err } @@ -46,20 +45,20 @@ func (t shellcheckTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t shellcheckTask) Post() error { return nil } +func (t shellcheckTask) Post(_ context.Context) error { return nil } // InfoText implements [tools.Tasker.InfoText]. func (t shfmtTask) InfoText() string { return "Format (shfmt)" } // Run implements [tools.Tasker.Run]. -func (t shfmtTask) Run() error { +func (t shfmtTask) Run(ctx context.Context) error { args := []string{"bash", "-c", fmt.Sprintf(` shopt -s globstar ls **/*.sh || exit 0 %s **/*.sh`, shfmt.Name, )} - if _, err := tools.RunCommand(args); err != nil { + if _, err := tools.RunCommand(ctx, args); err != nil { return err } @@ -67,26 +66,4 @@ func (t shfmtTask) Run() error { } // Post implements [tools.Tasker.Post]. -func (t shfmtTask) Post() error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t batsTask) InfoText() string { return "Test (bats)" } - -// Run implements [tools.Tasker.Run]. -func (t batsTask) Run() error { - args := []string{"bash", "-c", fmt.Sprintf(` - shopt -s globstar - # Don't run if no bats files found, otherwise it will error out - ls **/*.bats || exit 0 - %s **/*.bats`, - bats.Name, - )} - if _, err := tools.RunCommand(args); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t batsTask) Post() error { return nil } +func (t shfmtTask) Post(_ context.Context) error { return nil } diff --git a/internal/tools/shell/versions.go b/internal/tools/shell/versions.go index deedb7c..bec2cc4 100644 --- a/internal/tools/shell/versions.go +++ b/internal/tools/shell/versions.go @@ -1,4 +1,4 @@ -package shell +package shtools import ( "github.com/opensourcecorp/oscar/internal/tools" @@ -13,10 +13,4 @@ var ( Name: "shfmt", Version: "v3.12.0", } - bats = tools.Tool{ - Name: "bats", - // NOTE: bats just gets cloned then installed with its install script - RemotePath: "https://github.com/bats-core/bats-core.git", - Version: "v1.12.0", - } ) diff --git a/internal/tools/toolcfg/.markdownlint-cli2.yaml b/internal/tools/toolcfg/.markdownlint-cli2.yaml index ca080cd..7259a38 100644 --- a/internal/tools/toolcfg/.markdownlint-cli2.yaml +++ b/internal/tools/toolcfg/.markdownlint-cli2.yaml @@ -1,3 +1,4 @@ +--- config: default: true MD013: diff --git a/internal/tools/toolcfg/.yamlfmt b/internal/tools/toolcfg/.yamlfmt new file mode 100644 index 0000000..6e7519f --- /dev/null +++ b/internal/tools/toolcfg/.yamlfmt @@ -0,0 +1,5 @@ +--- +formatter: + type: "basic" + line_ending: "lf" + include_document_start: true diff --git a/internal/tools/toolcfg/.yamllint b/internal/tools/toolcfg/.yamllint new file mode 100644 index 0000000..5ebd35e --- /dev/null +++ b/internal/tools/toolcfg/.yamllint @@ -0,0 +1,15 @@ +--- +extends: "default" +rules: + line-length: + max: 100 + level: "error" + empty-lines: + max: 1 + max-start: 0 + max-end: 0 + level: "error" + quoted-strings: + required: true + quote-type: "double" + level: "error" diff --git a/internal/tools/toolcfg/embed.go b/internal/tools/toolcfg/embed.go index 0796b85..7e4547a 100644 --- a/internal/tools/toolcfg/embed.go +++ b/internal/tools/toolcfg/embed.go @@ -6,5 +6,5 @@ import "embed" // Files stores config files for each CI tool. // -//go:embed *.conf *.toml *.yaml +//go:embed * var Files embed.FS diff --git a/internal/tools/types.go b/internal/tools/types.go index db76167..cb61bc4 100644 --- a/internal/tools/types.go +++ b/internal/tools/types.go @@ -1,5 +1,7 @@ package tools +import "context" + // Tasker defines the method set for working with metadata for a given CI Task. type Tasker interface { // InfoText should return a human-readable display string that describes the task, e.g. "Run @@ -7,17 +9,19 @@ type Tasker interface { // may be desirable) in the case of implementers of [Tasker.Init]) InfoText() string // Run should perform the actual task's actions. - Run() error + Run(ctx context.Context) error // Post should perform any post-run actions for the task, if necessary. - Post() error + Post(ctx context.Context) error } // Repo stores information about the contents of the repository being ran against. type Repo struct { - HasGo bool - HasPython bool - HasShell bool - HasMarkdown bool + HasGo bool + HasPython bool + HasShell bool + HasTerraform bool + HasYaml bool + HasMarkdown bool } // TaskMap is a less-verbose type alias for mapping language names to function signatures that @@ -34,13 +38,13 @@ type Tool struct { // The tool's name, used as an identifier. May also be the tool's invocable command, in which // case it can be interpolated as such. Name string - // The installable path for the tool, like a URL. Can also be a format string, e.g. with - // placeholders for platform-specific strings. - RemotePath string - // The version of the tool. - Version string // The path to the tool's config file, if it has one to use. ConfigFilePath string + // The optional installable path for the tool, like a URL. Can also be a format string, e.g. + // with placeholders for platform-specific strings. Should mostly not be needed if using mise. + RemotePath string + // The version of the tool. Should mostly not be needed if using mise. + Version string } // String implements the [fmt.Stringer] interface. @@ -58,6 +62,12 @@ func (repo Repo) String() string { if repo.HasShell { out += "- Shell (sh, bash, etc.)\n" } + if repo.HasTerraform { + out += "- Terraform\n" + } + if repo.HasYaml { + out += "- YAML\n" + } if repo.HasMarkdown { out += "- Markdown\n" } diff --git a/internal/tools/util.go b/internal/tools/util.go index 0732e1b..c9a758a 100644 --- a/internal/tools/util.go +++ b/internal/tools/util.go @@ -1,6 +1,7 @@ package tools import ( + "context" "errors" "fmt" "io" @@ -18,7 +19,7 @@ import ( ) // InitSystem runs setup & checks against the host itself, so that oscar can run. -func InitSystem() error { +func InitSystem(ctx context.Context) error { fmt.Printf("Initializing the host, this might take some time... ") startTime := time.Now() @@ -29,7 +30,7 @@ func InitSystem() error { for _, cmd := range requiredSystemCommands { iprint.Debugf("Running '%v'\n", cmd) - if output, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput(); err != nil { + if output, err := exec.CommandContext(ctx, cmd[0], cmd[1:]...).CombinedOutput(); err != nil { return fmt.Errorf( "command '%s' possibly not found on PATH, cannot continue (error: %w -- output: %s)", cmd[0], err, string(output), @@ -55,7 +56,7 @@ func InitSystem() error { } } - if err := installMise(); err != nil { + if err := installMise(ctx); err != nil { return fmt.Errorf("installing mise: %w", err) } @@ -69,10 +70,10 @@ func InitSystem() error { } // Init for task runs - if _, err := RunCommand([]string{consts.MiseBinPath, "trust", consts.MiseConfigFileName}); err != nil { + if _, err := RunCommand(ctx, []string{consts.MiseBinPath, "trust", consts.MiseConfigFileName}); err != nil { return fmt.Errorf("running mise trust: %w", err) } - if _, err := RunCommand([]string{consts.MiseBinPath, "install"}); err != nil { + if _, err := RunCommand(ctx, []string{consts.MiseBinPath, "install"}); err != nil { return fmt.Errorf("running mise install: %w", err) } @@ -84,7 +85,7 @@ func InitSystem() error { // RunCommand takes a string slice containing an entire command & its args to run, and returns a // consistent error message in case of failure. It also returns the command output, in case the // caller needs to parse it on their own. -func RunCommand(cmdArgs []string) (string, error) { +func RunCommand(ctx context.Context, cmdArgs []string) (string, error) { if len(cmdArgs) <= 1 { return "", fmt.Errorf("internal error: not enough arguments passed to RunCommand() -- received: %v", cmdArgs) } @@ -96,7 +97,7 @@ func RunCommand(cmdArgs []string) (string, error) { args = slices.Concat([]string{"exec", "--"}, cmdArgs) } - cmd := exec.Command(consts.MiseBinPath, args...) + cmd := exec.CommandContext(ctx, consts.MiseBinPath, args...) iprint.Debugf("Running '%v'\n", cmd.Args) output, err := cmd.CombinedOutput() if err != nil { @@ -110,25 +111,35 @@ func RunCommand(cmdArgs []string) (string, error) { } // GetRepoComposition returns a populated [Repo]. -func GetRepoComposition() (Repo, error) { +func GetRepoComposition(ctx context.Context) (Repo, error) { var errs error - hasGo, err := filesExistInTree(`ls **/*.go`) + hasGo, err := filesExistInTree(ctx, GetFileTypeListerCommand("go")) if err != nil { errs = errors.Join(errs, err) } - hasPython, err := filesExistInTree(`find . -type f -name '*.py' -or -name '*.pyi'`) + hasPython, err := filesExistInTree(ctx, GetFileTypeListerCommand("py")) if err != nil { errs = errors.Join(errs, err) } - hasShell, err := filesExistInTree(`find . -type f -name '*.*sh' -or -name '*.bats'`) + hasTerraform, err := filesExistInTree(ctx, GetFileTypeListerCommand("tf")) if err != nil { errs = errors.Join(errs, err) } - hasMarkdown, err := filesExistInTree(`ls **/*.md`) + hasShell, err := filesExistInTree(ctx, GetFileTypeListerCommand("sh")) + if err != nil { + errs = errors.Join(errs, err) + } + + hasYaml, err := filesExistInTree(ctx, GetFileTypeListerCommand("yaml")) + if err != nil { + errs = errors.Join(errs, err) + } + + hasMarkdown, err := filesExistInTree(ctx, GetFileTypeListerCommand("md")) if err != nil { errs = errors.Join(errs, err) } @@ -138,16 +149,23 @@ func GetRepoComposition() (Repo, error) { } repo := Repo{ - HasGo: hasGo, - HasPython: hasPython, - HasShell: hasShell, - HasMarkdown: hasMarkdown, + HasGo: hasGo, + HasPython: hasPython, + HasShell: hasShell, + HasTerraform: hasTerraform, + HasYaml: hasYaml, + HasMarkdown: hasMarkdown, } iprint.Debugf("repo composition: %+v\n", repo) return repo, nil } +// ripgrep file-type spec. Used because it supports gitignoreables +func GetFileTypeListerCommand(fileType string) string { + return fmt.Sprintf(`rg --files --type '%s' || true`, fileType) +} + // RunDurationString returns a calculated duration used to indicate how long a particular task took // to run. func RunDurationString(t time.Time) string { @@ -156,7 +174,7 @@ func RunDurationString(t time.Time) string { // installMise determines if mise needs to be installed on the host, and if so, installs it into // [consts.OscarHomeBin]. -func installMise() (err error) { +func installMise(_ context.Context) (err error) { miseFound := true _, err = os.Stat(consts.MiseBinPath) if err != nil { @@ -206,6 +224,7 @@ func installMise() (err error) { } }() + // TODO: use a context func instead resp, err := http.Get(miseReleaseURL) if err != nil { return fmt.Errorf("making GET request for mise GitHub Release: %w", err) @@ -233,8 +252,8 @@ func installMise() (err error) { // filesExistInTree performs file discovery by allowing various tools to check if they need to run // based on file presence. -func filesExistInTree(findScript string) (bool, error) { - cmd := exec.Command("bash", "-c", fmt.Sprintf(` +func filesExistInTree(ctx context.Context, findScript string) (bool, error) { + cmd := exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf(` shopt -s globstar %s`, findScript, diff --git a/internal/tools/version/doc.go b/internal/tools/version/doc.go index 88f2965..759dafd 100644 --- a/internal/tools/version/doc.go +++ b/internal/tools/version/doc.go @@ -1,2 +1,2 @@ -// Package version contains logic for running tasks against the codebase version identifier(s). -package version +// Package versiontools contains logic for running tasks against the codebase version identifier(s). +package versiontools diff --git a/internal/tools/version/version.go b/internal/tools/version/version.go index b88c0ed..6e52efe 100644 --- a/internal/tools/version/version.go +++ b/internal/tools/version/version.go @@ -1,6 +1,7 @@ -package version +package versiontools import ( + "context" "errors" "fmt" "os" @@ -28,7 +29,7 @@ func TasksForCI(_ tools.Repo) []tools.Tasker { func (t versionCI) InfoText() string { return "Versioning checks" } // Run implements [tools.Tasker.Run]. -func (t versionCI) Run() (err error) { +func (t versionCI) Run(ctx context.Context) (err error) { cfg, err := oscarcfg.Get() if err != nil { return fmt.Errorf("getting oscar config: %w", err) @@ -50,12 +51,12 @@ func (t versionCI) Run() (err error) { } }() - remote, err := tools.RunCommand([]string{"git", "remote", "get-url", "origin"}) + remote, err := tools.RunCommand(ctx, []string{"git", "remote", "get-url", "origin"}) if err != nil { return fmt.Errorf("determining git root: %w", err) } - if _, err := tools.RunCommand([]string{"git", "clone", "--depth", "1", remote, tmpCloneDir}); err != nil { + if _, err := tools.RunCommand(ctx, []string{"git", "clone", "--depth", "1", remote, tmpCloneDir}); err != nil { return fmt.Errorf("cloning repo source to temp location: %w", err) } @@ -71,7 +72,7 @@ func (t versionCI) Run() (err error) { // // TODO: update internal git package to have a type with ALL this info so I stop copy-pasting // shell-outs around - branch, err := tools.RunCommand([]string{"git", "rev-parse", "--abbrev-ref", "HEAD"}) + branch, err := tools.RunCommand(ctx, []string{"git", "rev-parse", "--abbrev-ref", "HEAD"}) if err != nil { return fmt.Errorf("checking current Git branch/ref: %w", err) } @@ -90,4 +91,4 @@ func (t versionCI) Run() (err error) { } // Post implements [tools.Tasker.Post]. -func (t versionCI) Post() error { return nil } +func (t versionCI) Post(_ context.Context) error { return nil } diff --git a/internal/tools/yaml/doc.go b/internal/tools/yaml/doc.go new file mode 100644 index 0000000..de64d48 --- /dev/null +++ b/internal/tools/yaml/doc.go @@ -0,0 +1,2 @@ +// Package yamltools contains logic for running tasks for YAML. +package yamltools diff --git a/internal/tools/yaml/tasks.go b/internal/tools/yaml/tasks.go new file mode 100644 index 0000000..6dd3cb2 --- /dev/null +++ b/internal/tools/yaml/tasks.go @@ -0,0 +1,92 @@ +package yamltools + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/opensourcecorp/oscar/internal/tools" + "github.com/opensourcecorp/oscar/internal/tools/toolcfg" +) + +type ( + yamlfmtTask struct{} + yamllintTask struct{} +) + +var tasks = []tools.Tasker{ + yamlfmtTask{}, + yamllintTask{}, +} + +// Tasks returns the list of CI tasks. +func Tasks(repo tools.Repo) []tools.Tasker { + if repo.HasYaml { + return tasks + } + + return nil +} + +// InfoText implements [tools.Tasker.InfoText]. +func (t yamllintTask) InfoText() string { return "Lint (yamllint)" } + +// Run implements [tools.Tasker.Run]. +func (t yamllintTask) Run(ctx context.Context) error { + cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(yamllint.ConfigFilePath)) + if err != nil { + return fmt.Errorf("reading embedded file contents: %w", err) + } + + if err := os.WriteFile(yamllint.ConfigFilePath, cfgFileContents, 0644); err != nil { + return fmt.Errorf("writing config file: %w", err) + } + + args := []string{"bash", "-c", + fmt.Sprintf( + `%s --strict --config-file %s $(%s)`, + yamllint.Name, yamllint.ConfigFilePath, tools.GetFileTypeListerCommand("yaml"), + ), + } + + if _, err := tools.RunCommand(ctx, args); err != nil { + return err + } + + return nil +} + +// Post implements [tools.Tasker.Post]. +func (t yamllintTask) Post(_ context.Context) error { return nil } + +// InfoText implements [tools.Tasker.InfoText]. +func (t yamlfmtTask) InfoText() string { return "Format (yamlfmt)" } + +// Run implements [tools.Tasker.Run]. +func (t yamlfmtTask) Run(ctx context.Context) error { + cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(yamlfmt.ConfigFilePath)) + if err != nil { + return fmt.Errorf("reading embedded file contents: %w", err) + } + + if err := os.WriteFile(yamlfmt.ConfigFilePath, cfgFileContents, 0644); err != nil { + return fmt.Errorf("writing config file: %w", err) + } + + args := []string{"bash", "-c", + fmt.Sprintf( + `%s -conf %s $(%s)`, + yamlfmt.Name, yamlfmt.ConfigFilePath, tools.GetFileTypeListerCommand("yaml"), + ), + } + + if _, err := tools.RunCommand(ctx, args); err != nil { + return err + } + + return nil +} + +// Post implements [tools.Tasker.Post]. +func (t yamlfmtTask) Post(_ context.Context) error { return nil } diff --git a/internal/tools/yaml/versions.go b/internal/tools/yaml/versions.go new file mode 100644 index 0000000..25513ed --- /dev/null +++ b/internal/tools/yaml/versions.go @@ -0,0 +1,19 @@ +package yamltools + +import ( + "os" + "path/filepath" + + "github.com/opensourcecorp/oscar/internal/tools" +) + +var ( + yamlfmt = tools.Tool{ + Name: "yamlfmt", + ConfigFilePath: filepath.Join(os.TempDir(), ".yamlfmt"), + } + yamllint = tools.Tool{ + Name: "yamllint", + ConfigFilePath: filepath.Join(os.TempDir(), ".yamllint"), + } +) diff --git a/mise.toml b/mise.toml index f5630be..c314308 100644 --- a/mise.toml +++ b/mise.toml @@ -1,9 +1,19 @@ [tools] -bats = "v1.12.0" -github-cli = "2.78.0" -go = "1.25.0" -markdownlint-cli2 = "v0.18.1" # required for markdownlint-cli2 -node = "24.7.0" -shellcheck = "v0.11.0" -shfmt = "v3.12.0" -uv = "0.8.14" +github-cli = "prefix:2" +go = "prefix:1" +markdownlint-cli2 = "prefix:0.18" +# required for markdownlint-cli2 +node = "prefix:24" +ripgrep = "prefix:14" +shellcheck = "prefix:0.11" +shfmt = "prefix:3" +uv = "prefix:0.8" +yamlfmt = "prefix:0.17" +yamllint = "prefix:1" + +# Go tools +"go:honnef.co/go/tools/cmd/staticcheck" = { version = "latest" } +"go:github.com/mgechev/revive" = { version = "prefix:1" } +"go:github.com/kisielk/errcheck" = { version = "prefix:1" } +"go:golang.org/x/tools/cmd/goimports" = { version = "latest" } +"go:golang.org/x/vuln/cmd/govulncheck" = { version = "prefix:1" } diff --git a/oscar.yaml b/oscar.yaml index 6081430..b4a659f 100644 --- a/oscar.yaml +++ b/oscar.yaml @@ -1,3 +1,4 @@ +--- version: "0.2.0" deliver: go_github_release: From 162574fec95c3a28bce27e75c113d7b87c31d63e Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sat, 13 Sep 2025 22:28:59 -0500 Subject: [PATCH 07/38] Delete VERSION file, add Draft field for GH Release struct --- Makefile | 2 +- VERSION | 1 - go.mod | 3 ++- go.sum | 4 ++-- internal/oscarcfg/config.go | 5 ++++- internal/oscarcfg/test.oscar.yaml | 1 + oscar.yaml | 1 + 7 files changed, 11 insertions(+), 6 deletions(-) delete mode 100644 VERSION diff --git a/Makefile b/Makefile index a0ff4a0..a08fa7d 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ ci-container: -t $(BINNAME)-test:latest \ . -deliver: ci +deliver: @$(RUN) go run ./cmd/$(BINNAME)/main.go deliver clean: diff --git a/VERSION b/VERSION deleted file mode 100644 index 0ea3a94..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.2.0 diff --git a/go.mod b/go.mod index b840406..5735a9d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/opensourcecorp/oscar go 1.24.4 require ( - github.com/goccy/go-yaml v1.18.0 github.com/urfave/cli/v3 v3.4.1 golang.org/x/mod v0.27.0 ) + +require go.yaml.in/yaml/v4 v4.0.0-rc.2 diff --git a/go.sum b/go.sum index 741856c..d5599e6 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= +go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/oscarcfg/config.go b/internal/oscarcfg/config.go index 463de9a..379882b 100644 --- a/internal/oscarcfg/config.go +++ b/internal/oscarcfg/config.go @@ -4,9 +4,9 @@ import ( "fmt" "os" - "github.com/goccy/go-yaml" "github.com/opensourcecorp/oscar/internal/consts" iprint "github.com/opensourcecorp/oscar/internal/print" + "go.yaml.in/yaml/v4" ) // Config defines the top-level structure of oscar's config file. @@ -38,6 +38,9 @@ type GoGitHubRelease struct { Repo string `yaml:"repo" json:"repo"` // BuildSources are the filepaths to the "main" packages to be built. BuildSources []string `yaml:"build_sources" json:"build_sources"` + // Draft flags whether the Release should be left in Draft state at create-time. This can be + // useful to set if you want to review the Release contents before actually publishing. + Draft bool } // Get returns a populated [Config] based on the oscar config file location. If `path` is not diff --git a/internal/oscarcfg/test.oscar.yaml b/internal/oscarcfg/test.oscar.yaml index 00d06fd..3e20920 100644 --- a/internal/oscarcfg/test.oscar.yaml +++ b/internal/oscarcfg/test.oscar.yaml @@ -7,3 +7,4 @@ deliver: repo: "test" build_sources: - "./cmd/test" + draft: false diff --git a/oscar.yaml b/oscar.yaml index b4a659f..7a85297 100644 --- a/oscar.yaml +++ b/oscar.yaml @@ -5,3 +5,4 @@ deliver: repo: "opensourcecorp/oscar" build_sources: - "./cmd/oscar" + draft: false From 7f6344791df2965063ee671a50ded6c18f8137f2 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 14 Sep 2025 20:18:11 -0500 Subject: [PATCH 08/38] Get all kinds of stuff working after a slight refactor caused a cascade of changes. --- .dockerignore | 3 - Containerfile | 18 +- docker-compose.yaml | 2 +- internal/git/ci.go | 4 +- internal/git/deliver.go | 6 +- internal/tasks/ci/run.go | 112 +++---- internal/tasks/delivery/run.go | 89 ++---- internal/tasks/tools/containerfile/ci.go | 55 ++++ internal/tasks/tools/containerfile/doc.go | 2 + internal/tasks/tools/go/ci.go | 281 ++++++++++++++++++ internal/{ => tasks}/tools/go/deliver.go | 26 +- internal/{ => tasks}/tools/go/doc.go | 0 internal/tasks/tools/markdown/ci.go | 49 +++ internal/{ => tasks}/tools/markdown/doc.go | 0 internal/tasks/tools/python/ci.go | 125 ++++++++ internal/{ => tasks}/tools/python/doc.go | 0 internal/tasks/tools/shell/ci.go | 70 +++++ internal/{ => tasks}/tools/shell/doc.go | 0 .../tools/toolcfg/.markdownlint-cli2.yaml | 0 internal/tasks/tools/toolcfg/.yamlfmt | 8 + internal/{ => tasks}/tools/toolcfg/.yamllint | 1 + .../embed.go => tasks/tools/toolcfg/doc.go} | 7 - internal/tasks/tools/toolcfg/embed.go | 8 + internal/tasks/tools/toolcfg/hadolint.yaml | 9 + .../{ => tasks}/tools/toolcfg/pyproject.toml | 0 .../{ => tasks}/tools/toolcfg/revive.toml | 0 .../tools/toolcfg/staticcheck.conf | 0 internal/tasks/tools/toolcfg/util.go | 24 ++ .../version.go => tasks/tools/version/ci.go} | 43 ++- internal/tasks/tools/version/ci_test.go | 13 + internal/{ => tasks}/tools/version/doc.go | 0 internal/tasks/tools/yaml/ci.go | 86 ++++++ internal/{ => tasks}/tools/yaml/doc.go | 0 internal/tasks/util/doc.go | 3 + internal/{tools => tasks/util}/types.go | 71 +++-- internal/{tools => tasks/util}/util.go | 104 +++++-- internal/tools/doc.go | 2 - internal/tools/go/ci.go | 271 ----------------- internal/tools/markdown/tasks.go | 58 ---- internal/tools/markdown/versions.go | 15 - internal/tools/python/tasks.go | 136 --------- internal/tools/python/versions.go | 26 -- internal/tools/shell/tasks.go | 69 ----- internal/tools/shell/versions.go | 16 - internal/tools/toolcfg/.yamlfmt | 5 - internal/tools/yaml/tasks.go | 92 ------ internal/tools/yaml/versions.go | 19 -- mise.toml | 19 +- scripts/test-bootstrap.sh | 2 +- 49 files changed, 1011 insertions(+), 938 deletions(-) delete mode 100644 .dockerignore create mode 100644 internal/tasks/tools/containerfile/ci.go create mode 100644 internal/tasks/tools/containerfile/doc.go create mode 100644 internal/tasks/tools/go/ci.go rename internal/{ => tasks}/tools/go/deliver.go (71%) rename internal/{ => tasks}/tools/go/doc.go (100%) create mode 100644 internal/tasks/tools/markdown/ci.go rename internal/{ => tasks}/tools/markdown/doc.go (100%) create mode 100644 internal/tasks/tools/python/ci.go rename internal/{ => tasks}/tools/python/doc.go (100%) create mode 100644 internal/tasks/tools/shell/ci.go rename internal/{ => tasks}/tools/shell/doc.go (100%) rename internal/{ => tasks}/tools/toolcfg/.markdownlint-cli2.yaml (100%) create mode 100644 internal/tasks/tools/toolcfg/.yamlfmt rename internal/{ => tasks}/tools/toolcfg/.yamllint (76%) rename internal/{tools/toolcfg/embed.go => tasks/tools/toolcfg/doc.go} (56%) create mode 100644 internal/tasks/tools/toolcfg/embed.go create mode 100644 internal/tasks/tools/toolcfg/hadolint.yaml rename internal/{ => tasks}/tools/toolcfg/pyproject.toml (100%) rename internal/{ => tasks}/tools/toolcfg/revive.toml (100%) rename internal/{ => tasks}/tools/toolcfg/staticcheck.conf (100%) create mode 100644 internal/tasks/tools/toolcfg/util.go rename internal/{tools/version/version.go => tasks/tools/version/ci.go} (67%) create mode 100644 internal/tasks/tools/version/ci_test.go rename internal/{ => tasks}/tools/version/doc.go (100%) create mode 100644 internal/tasks/tools/yaml/ci.go rename internal/{ => tasks}/tools/yaml/doc.go (100%) create mode 100644 internal/tasks/util/doc.go rename internal/{tools => tasks/util}/types.go (53%) rename internal/{tools => tasks/util}/util.go (66%) delete mode 100644 internal/tools/doc.go delete mode 100644 internal/tools/go/ci.go delete mode 100644 internal/tools/markdown/tasks.go delete mode 100644 internal/tools/markdown/versions.go delete mode 100644 internal/tools/python/tasks.go delete mode 100644 internal/tools/python/versions.go delete mode 100644 internal/tools/shell/tasks.go delete mode 100644 internal/tools/shell/versions.go delete mode 100644 internal/tools/toolcfg/.yamlfmt delete mode 100644 internal/tools/yaml/tasks.go delete mode 100644 internal/tools/yaml/versions.go diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 1784d3b..0000000 --- a/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -Containerfile -docker-compose.yaml -*.md diff --git a/Containerfile b/Containerfile index c022c91..1aae2fd 100644 --- a/Containerfile +++ b/Containerfile @@ -10,10 +10,12 @@ ARG https_proxy FROM docker.io/library/golang:${GO_VERSION} AS builder -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install --no-install-recommends -y \ bash \ ca-certificates \ - make + make \ + && \ + rm -rf /var/lib/apt* COPY . /go/app WORKDIR /go/app @@ -26,14 +28,16 @@ FROM docker.io/library/debian:13-slim AS ci COPY --from=builder /go/app/build/oscar /oscar -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install --no-install-recommends -y \ bash \ ca-certificates \ curl \ git \ gnupg2 \ make \ - rename + rename \ + && \ + rm -rf /var/lib/apt/* COPY . /go/app WORKDIR /go/app @@ -56,11 +60,13 @@ COPY --from=builder /go/app/build/oscar /oscar # stage above to run, we need to force a dependency here COPY --from=ci /go/app/oscar.yaml /oscar.yaml -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install --no-install-recommends -y \ bash \ ca-certificates \ git \ - gnupg2 + gnupg2 \ + && \ + rm -rf /var/lib/apt/* # NOTE: when creating some shims, mise refers to itself assuming it is on the $PATH, so we need to # symlink it out so it can do that diff --git a/docker-compose.yaml b/docker-compose.yaml index bed77fd..d7f4a9b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,7 +7,7 @@ services: dockerfile: "./Containerfile" args: GO_VERSION: "${GO_VERSION:-1.25.0}" - MISE_VERSION: "${MISE_VERSION:-v2025.8.21}" + MISE_VERSION: "${MISE_VERSION:-v2025.9.10}" http_proxy: "${http_proxy}" https_proxy: "${https_proxy}" image: "ghrc.io/opensourcecorp/oscar:latest" diff --git a/internal/git/ci.go b/internal/git/ci.go index ecfd6bc..3b3b97a 100644 --- a/internal/git/ci.go +++ b/internal/git/ci.go @@ -8,7 +8,7 @@ import ( "strings" iprint "github.com/opensourcecorp/oscar/internal/print" - "github.com/opensourcecorp/oscar/internal/tools" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) // CI defines metadata & behavior for CI tasks. @@ -92,7 +92,7 @@ func (g *CI) StatusHasChanged(ctx context.Context) (bool, error) { // getRawStatus returns a slightly-modified "git status" output, so that calling tools can parse it // more easily. func getRawStatus(ctx context.Context) (Status, error) { - outputBytes, err := tools.RunCommand(ctx, []string{"git", "status", "--porcelain"}) + outputBytes, err := taskutil.RunCommand(ctx, []string{"git", "status", "--porcelain"}) if err != nil { return Status{}, fmt.Errorf("getting git status output: %w", err) } diff --git a/internal/git/deliver.go b/internal/git/deliver.go index ea60031..cbaa25d 100644 --- a/internal/git/deliver.go +++ b/internal/git/deliver.go @@ -6,7 +6,7 @@ import ( "github.com/opensourcecorp/oscar/internal/oscarcfg" iprint "github.com/opensourcecorp/oscar/internal/print" - "github.com/opensourcecorp/oscar/internal/tools" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) // Delivery defines metadata & behavior for Delivery tasks. @@ -19,12 +19,12 @@ type Delivery struct { // NewForDelivery returns Git information for Delivery tasks. func NewForDelivery(ctx context.Context) (*Delivery, error) { - root, err := tools.RunCommand(ctx, []string{"git", "rev-parse", "--show-toplevel"}) + root, err := taskutil.RunCommand(ctx, []string{"git", "rev-parse", "--show-toplevel"}) if err != nil { return nil, err } - latestTag, err := tools.RunCommand(ctx, []string{"bash", "-c", "git tag --list | tail -n1"}) + latestTag, err := taskutil.RunCommand(ctx, []string{"bash", "-c", "git tag --list | tail -n1"}) if err != nil { return nil, err } diff --git a/internal/tasks/ci/run.go b/internal/tasks/ci/run.go index 93cf4f3..99b966a 100644 --- a/internal/tasks/ci/run.go +++ b/internal/tasks/ci/run.go @@ -11,30 +11,33 @@ import ( "github.com/opensourcecorp/oscar/internal/consts" "github.com/opensourcecorp/oscar/internal/git" iprint "github.com/opensourcecorp/oscar/internal/print" - "github.com/opensourcecorp/oscar/internal/tools" - gotools "github.com/opensourcecorp/oscar/internal/tools/go" - mdtools "github.com/opensourcecorp/oscar/internal/tools/markdown" - pytools "github.com/opensourcecorp/oscar/internal/tools/python" - shtools "github.com/opensourcecorp/oscar/internal/tools/shell" - versiontools "github.com/opensourcecorp/oscar/internal/tools/version" - yamltools "github.com/opensourcecorp/oscar/internal/tools/yaml" + containerfiletools "github.com/opensourcecorp/oscar/internal/tasks/tools/containerfile" + gotools "github.com/opensourcecorp/oscar/internal/tasks/tools/go" + mdtools "github.com/opensourcecorp/oscar/internal/tasks/tools/markdown" + pytools "github.com/opensourcecorp/oscar/internal/tasks/tools/python" + shtools "github.com/opensourcecorp/oscar/internal/tasks/tools/shell" + versiontools "github.com/opensourcecorp/oscar/internal/tasks/tools/version" + yamltools "github.com/opensourcecorp/oscar/internal/tasks/tools/yaml" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) -// GetCITaskMap assembles the overall list of CI tasks, keyed by their language/tooling name -func GetCITaskMap(ctx context.Context) (tools.TaskMap, error) { - repo, err := tools.GetRepoComposition(ctx) +// getCITasksMap assembles the overall list of CI tasks, keyed by their language/tooling name +func getCITasksMap(ctx context.Context) (taskutil.TasksMap, error) { + repo, err := taskutil.GetRepoComposition(ctx) if err != nil { return nil, fmt.Errorf("getting repo composition: %w", err) } - out := make(tools.TaskMap, 0) - for langName, getTasksFunc := range map[string]func(tools.Repo) []tools.Tasker{ - "Version": versiontools.TasksForCI, - "Go": gotools.TasksForCI, - "Python": pytools.Tasks, - "YAML": yamltools.Tasks, - "Shell": shtools.Tasks, - "Markdown": mdtools.Tasks, + out := make(taskutil.TasksMap) + for langName, getTasksFunc := range map[string]func(taskutil.Repo) []taskutil.Tasker{ + "Versioning": versiontools.NewTasksForCI, + "Go": gotools.NewTasksForCI, + "Python": pytools.NewTasksForCI, + // "Terraform": tftools.NewTasksForCI, + "YAML": yamltools.NewTasksForCI, + "Containerfile": containerfiletools.NewTasksForCI, + "Shell": shtools.NewTasksForCI, + "Markdown": mdtools.NewTasksForCI, } { tasks := getTasksFunc(repo) if len(tasks) > 0 { @@ -44,7 +47,7 @@ func GetCITaskMap(ctx context.Context) (tools.TaskMap, error) { if len(out) > 0 { fmt.Print(repo.String()) - iprint.Debugf("GetCITasks output: %#v\n", out) + iprint.Debugf("getCITasksMap output: %#v\n", out) } return out, nil @@ -52,76 +55,47 @@ func GetCITaskMap(ctx context.Context) (tools.TaskMap, error) { // Run defines the behavior for running all CI tasks for the repository. func Run(ctx context.Context) (err error) { - runStartTime := time.Now() - - // Handle system init - if err := tools.InitSystem(ctx); err != nil { - return fmt.Errorf("initializing system: %w", err) - } // The mise config that oscar uses is written during init, so be sure to defer its removal here defer func() { - if rmErr := os.Remove(consts.MiseConfigFileName); rmErr != nil { + if rmErr := os.RemoveAll(consts.MiseConfigFileName); rmErr != nil { err = errors.Join(err, fmt.Errorf("removing mise config file: %w", rmErr)) } }() - var ( - // Vars for determining text padding in output banners - longestLanguageNameLength int - longestInfoTextLength int - ) - - // All the CI tasks that will be looped over. Will also print a summary of discovered file - // types. - ciTaskMap, err := GetCITaskMap(ctx) + tasksMap, err := getCITasksMap(ctx) if err != nil { - return fmt.Errorf("getting CI tasks: %w", err) + return err } - // Log padding setup - for lang, tasks := range ciTaskMap { - longestLanguageNameLength = max(longestLanguageNameLength, len(lang)) - for _, t := range tasks { - longestInfoTextLength = max(longestInfoTextLength, len(t.InfoText())) - } + run, err := taskutil.NewRun(ctx, tasksMap) + if err != nil { + return fmt.Errorf("internal error setting up run info: %w", err) } - iprint.Debugf("longestLanguageNameLength: %d\n", longestLanguageNameLength) - iprint.Debugf("longestInfoTextLength: %d\n", longestInfoTextLength) - - // For tracking any changes to Git status etc. after each Task runs + // For tracking any changes to Git status etc. after each CI Task runs gitCI, err := git.NewForCI(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } - // Keeps track of all task failures - failures := make([]string, 0) - for lang, tasks := range ciTaskMap { - langNameBannerPadding := strings.Repeat("=", longestLanguageNameLength-len(lang)/2) + for lang, tasks := range tasksMap { + langNameBannerPadding := strings.Repeat("=", run.LongestLanguageNameLength-len(lang)/2) fmt.Printf( "%s%s %s %s%s\n", strings.Repeat("=", 24), langNameBannerPadding, lang, langNameBannerPadding, strings.Repeat("=", 24), ) - - for _, t := range tasks { - // NOTE: if no InfoText() method is provided, it's probably a lang-wide init func, so - // skip it - if t.InfoText() == "" { - continue - } - + for _, task := range tasks { taskStartTime := time.Now() - taskBannerPadding := strings.Repeat(".", longestInfoTextLength-len(t.InfoText())) + taskBannerPadding := strings.Repeat(".", run.LongestInfoTextLength-len(task.InfoText())) // NOTE: no trailing newline on purpose - fmt.Printf("> %s %s............", t.InfoText(), taskBannerPadding) + fmt.Printf("> %s %s............", task.InfoText(), taskBannerPadding) // NOTE: this error is checked later, when we can check the Run, Post, and git-diff // potential errors together var runErr error - runErr = errors.Join(runErr, t.Run(ctx)) - runErr = errors.Join(runErr, t.Post(ctx)) + runErr = errors.Join(runErr, task.Exec(ctx)) + runErr = errors.Join(runErr, task.Post(ctx)) if err := gitCI.Update(ctx); err != nil { return fmt.Errorf("internal error: %w", err) @@ -132,7 +106,7 @@ func Run(ctx context.Context) (err error) { } if runErr != nil || gitStatusHasChanged { - iprint.Errorf("FAILED (%s)\n", tools.RunDurationString(taskStartTime)) + iprint.Errorf("FAILED (%s)\n", taskutil.RunDurationString(taskStartTime)) iprint.Errorf("\n") if runErr != nil { @@ -145,7 +119,7 @@ func Run(ctx context.Context) (err error) { iprint.Errorf("\n") } - failures = append(failures, fmt.Sprintf("%s :: %s", lang, t.InfoText())) + run.Failures = append(run.Failures, fmt.Sprintf("%s :: %s", lang, task.InfoText())) // Also need to reset the baseline status gitCI, err = git.NewForCI(ctx) @@ -153,22 +127,22 @@ func Run(ctx context.Context) (err error) { return fmt.Errorf("internal error: %w", err) } } else { - fmt.Printf("PASSED (%s)\n", tools.RunDurationString(taskStartTime)) + fmt.Printf("PASSED (%s)\n", taskutil.RunDurationString(taskStartTime)) } } } - if len(failures) > 0 { + if len(run.Failures) > 0 { iprint.Errorf("\n================================================================\n") - iprint.Errorf("The following checks failed and/or caused a git diff: (%s)\n", tools.RunDurationString(runStartTime)) - for _, f := range failures { + iprint.Errorf("The following checks failed and/or caused a git diff: (%s)\n", taskutil.RunDurationString(run.StartTime)) + for _, f := range run.Failures { iprint.Errorf("- %s\n", f) } iprint.Errorf("================================================================\n\n") return errors.New("one or more CI checks failed") } - fmt.Printf("All checks passed! (%s)\n", tools.RunDurationString(runStartTime)) + fmt.Printf("All checks passed! (%s)\n", taskutil.RunDurationString(run.StartTime)) return err } diff --git a/internal/tasks/delivery/run.go b/internal/tasks/delivery/run.go index a6c1a0d..94107e7 100644 --- a/internal/tasks/delivery/run.go +++ b/internal/tasks/delivery/run.go @@ -10,21 +10,26 @@ import ( "github.com/opensourcecorp/oscar/internal/consts" iprint "github.com/opensourcecorp/oscar/internal/print" - "github.com/opensourcecorp/oscar/internal/tools" - gotools "github.com/opensourcecorp/oscar/internal/tools/go" + gotools "github.com/opensourcecorp/oscar/internal/tasks/tools/go" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) -// GetDeliveryTaskMap assembles the overall list of Delivery tasks, keyed by their language/tooling +// getDeliveryTasksMap assembles the overall list of Delivery tasks, keyed by their language/tooling // name. -func GetDeliveryTaskMap(ctx context.Context) (tools.TaskMap, error) { - repo, err := tools.GetRepoComposition(ctx) +func getDeliveryTasksMap(ctx context.Context) (taskutil.TasksMap, error) { + repo, err := taskutil.GetRepoComposition(ctx) if err != nil { return nil, fmt.Errorf("getting repo composition: %w", err) } - out := make(tools.TaskMap, 0) - for langName, getTasksFunc := range map[string]func(tools.Repo) []tools.Tasker{ + out := make(taskutil.TasksMap) + for langName, getTasksFunc := range map[string]func(taskutil.Repo) []taskutil.Tasker{ + // "Version": versiontools.TasksForDelivery, "Go": gotools.TasksForDelivery, + // "Python": pytools.TasksForDelivery, + // "YAML": yamltools.TasksForDelivery, + // "Shell": shtools.TasksForDelivery, + // "Markdown": mdtools.TasksForDelivery, } { tasks := getTasksFunc(repo) if len(tasks) > 0 { @@ -34,7 +39,7 @@ func GetDeliveryTaskMap(ctx context.Context) (tools.TaskMap, error) { if len(out) > 0 { fmt.Print(repo.String()) - iprint.Debugf("GetDeliveryTaskMap output: %#v\n", out) + iprint.Debugf("getDeliveryTasksMap output: %#v\n", out) } return out, nil @@ -42,93 +47,65 @@ func GetDeliveryTaskMap(ctx context.Context) (tools.TaskMap, error) { // Run defines the behavior for running all Delivery tasks for the repository. func Run(ctx context.Context) (err error) { - runStartTime := time.Now() - - // Handle system init - if err := tools.InitSystem(ctx); err != nil { - return fmt.Errorf("initializing system: %w", err) - } // The mise config that oscar uses is written during init, so be sure to defer its removal here defer func() { - if rmErr := os.Remove(consts.MiseConfigFileName); rmErr != nil { + if rmErr := os.RemoveAll(consts.MiseConfigFileName); rmErr != nil { err = errors.Join(err, fmt.Errorf("removing mise config file: %w", rmErr)) } }() - var ( - // Vars for determining text padding in output banners - longestLanguageNameLength int - longestInfoTextLength int - ) - - // All the Delivery tasks that will be looped over. Will also print a summary of discovered file - // types. - deliveryTaskMap, err := GetDeliveryTaskMap(ctx) + tasksMap, err := getDeliveryTasksMap(ctx) if err != nil { - return fmt.Errorf("getting Delivery tasks: %w", err) + return err } - // Log padding setup - for lang, tasks := range deliveryTaskMap { - longestLanguageNameLength = max(longestLanguageNameLength, len(lang)) - for _, t := range tasks { - longestInfoTextLength = max(longestInfoTextLength, len(t.InfoText())) - } + run, err := taskutil.NewRun(ctx, tasksMap) + if err != nil { + return fmt.Errorf("internal error setting up run info: %w", err) } - iprint.Debugf("longestLanguageNameLength: %d\n", longestLanguageNameLength) - iprint.Debugf("longestInfoTextLength: %d\n", longestInfoTextLength) - - // Keeps track of all task failures - failures := make([]string, 0) - for lang, tasks := range deliveryTaskMap { - langNameBannerPadding := strings.Repeat("=", longestLanguageNameLength-len(lang)/2) + for lang, tasks := range tasksMap { + langNameBannerPadding := strings.Repeat("=", run.LongestLanguageNameLength-len(lang)/2) fmt.Printf( "%s%s %s %s%s\n", strings.Repeat("=", 24), langNameBannerPadding, lang, langNameBannerPadding, strings.Repeat("=", 24), ) - for _, t := range tasks { - // NOTE: if no InfoText() method is provided, it's probably a lang-wide init func, so - // skip it - if t.InfoText() == "" { - continue - } - + for _, task := range tasks { taskStartTime := time.Now() - taskBannerPadding := strings.Repeat(".", longestInfoTextLength-len(t.InfoText())) + taskBannerPadding := strings.Repeat(".", run.LongestInfoTextLength-len(task.InfoText())) // NOTE: no trailing newline on purpose - fmt.Printf("> %s %s............", t.InfoText(), taskBannerPadding) + fmt.Printf("> %s %s............", task.InfoText(), taskBannerPadding) // NOTE: this error is checked later, when we can check the Run, Post, and git-diff // potential errors together var runErr error - runErr = errors.Join(runErr, t.Run(ctx)) - runErr = errors.Join(runErr, t.Post(ctx)) + runErr = errors.Join(runErr, task.Exec(ctx)) + runErr = errors.Join(runErr, task.Post(ctx)) if runErr != nil { - iprint.Errorf("FAILED (%s)\n", tools.RunDurationString(taskStartTime)) + iprint.Errorf("FAILED (%s)\n", taskutil.RunDurationString(taskStartTime)) iprint.Errorf("%v\n", runErr) - failures = append(failures, fmt.Sprintf("%s :: %s", lang, t.InfoText())) + run.Failures = append(run.Failures, fmt.Sprintf("%s :: %s", lang, task.InfoText())) } else { - fmt.Printf("SUCCEEDED (%s)\n", tools.RunDurationString(taskStartTime)) + fmt.Printf("SUCCEEDED (%s)\n", taskutil.RunDurationString(taskStartTime)) } } } - if len(failures) > 0 { + if len(run.Failures) > 0 { iprint.Errorf("\n================================================================\n") - iprint.Errorf("The following tasks failed: (%s)\n", tools.RunDurationString(runStartTime)) - for _, f := range failures { + iprint.Errorf("The following tasks failed: (%s)\n", taskutil.RunDurationString(run.StartTime)) + for _, f := range run.Failures { iprint.Errorf("- %s\n", f) } iprint.Errorf("================================================================\n\n") return errors.New("one or more Delivery tasks failed") } - fmt.Printf("All tasks succeeded! (%s)\n", tools.RunDurationString(runStartTime)) + fmt.Printf("All tasks succeeded! (%s)\n", taskutil.RunDurationString(run.StartTime)) return err } diff --git a/internal/tasks/tools/containerfile/ci.go b/internal/tasks/tools/containerfile/ci.go new file mode 100644 index 0000000..a1ff5a1 --- /dev/null +++ b/internal/tasks/tools/containerfile/ci.go @@ -0,0 +1,55 @@ +package containerfiletools + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/opensourcecorp/oscar/internal/tasks/tools/toolcfg" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" +) + +type ( + hadolint struct{ taskutil.Tool } +) + +// NewTasksForCI returns the list of CI tasks. +func NewTasksForCI(repo taskutil.Repo) []taskutil.Tasker { + if repo.HasContainerfile { + return []taskutil.Tasker{ + hadolint{ + Tool: taskutil.Tool{ + RunArgs: []string{"bash", "-c", + fmt.Sprintf( + `hadolint --config {{ConfigFilePath}} $(%s)`, + taskutil.GetFileTypeListerCommand("containerfile"), + ), + }, + ConfigFilePath: filepath.Join(os.TempDir(), "hadolint.yaml"), + }, + }, + } + } + + return nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t hadolint) InfoText() string { return "Lint (hadolint)" } + +// Run implements [taskutil.Tasker.Run]. +func (t hadolint) Exec(ctx context.Context) error { + if err := toolcfg.SetupConfigFile(t.Tool); err != nil { + return err + } + + if _, err := taskutil.RunCommand(ctx, t.RenderRunCommandArgs()); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t hadolint) Post(_ context.Context) error { return nil } diff --git a/internal/tasks/tools/containerfile/doc.go b/internal/tasks/tools/containerfile/doc.go new file mode 100644 index 0000000..2820883 --- /dev/null +++ b/internal/tasks/tools/containerfile/doc.go @@ -0,0 +1,2 @@ +// Package containerfiletools contains logic for running tasks for Containerfiles. +package containerfiletools diff --git a/internal/tasks/tools/go/ci.go b/internal/tasks/tools/go/ci.go new file mode 100644 index 0000000..fd844a5 --- /dev/null +++ b/internal/tasks/tools/go/ci.go @@ -0,0 +1,281 @@ +package gotools + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/opensourcecorp/oscar/internal/tasks/tools/toolcfg" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" +) + +type ( + goModCheck struct{ taskutil.Tool } + goFormat struct{ taskutil.Tool } + generateCodeCI struct{ taskutil.Tool } + goBuildCI struct{ taskutil.Tool } + goVet struct{ taskutil.Tool } + staticcheck struct{ taskutil.Tool } + revive struct{ taskutil.Tool } + errcheck struct{ taskutil.Tool } + goImports struct{ taskutil.Tool } + govulncheck struct{ taskutil.Tool } + goTest struct{ taskutil.Tool } +) + +// NewTasksForCI returns the list of CI tasks. +func NewTasksForCI(repo taskutil.Repo) []taskutil.Tasker { + if repo.HasGo { + return []taskutil.Tasker{ + goModCheck{ + Tool: taskutil.Tool{ + RunArgs: []string{"go", "mod", "tidy"}, + }, + }, + goFormat{ + Tool: taskutil.Tool{ + RunArgs: []string{"go", "fmt", "./..."}, + }, + }, + generateCodeCI{ + Tool: taskutil.Tool{ + RunArgs: []string{"go", "generate", "./..."}, + }, + }, + goBuildCI{ + Tool: taskutil.Tool{ + RunArgs: []string{"go", "build", "./..."}, + }, + }, + goVet{ + Tool: taskutil.Tool{ + RunArgs: []string{"go", "vet", "./..."}, + }, + }, + staticcheck{ + Tool: taskutil.Tool{ + RunArgs: []string{"staticcheck", "./..."}, + // NOTE: staticcheck does not have a flag to point to a config file, so we need + // to put it at the repo root + ConfigFilePath: filepath.Join("staticcheck.conf"), + }, + }, + revive{ + Tool: taskutil.Tool{ + RunArgs: []string{ + "revive", "--config", "{{ConfigFilePath}}", "--set_exit_status", "./...", + }, + ConfigFilePath: filepath.Join(os.TempDir(), "revive.toml"), + }, + }, + errcheck{ + Tool: taskutil.Tool{ + RunArgs: []string{"errcheck", "./..."}, + }, + }, + goImports{ + Tool: taskutil.Tool{ + RunArgs: []string{"goimports", "-l", "-w", "."}, + }, + }, + govulncheck{ + Tool: taskutil.Tool{ + RunArgs: []string{"govulncheck", "./..."}, + }, + }, + goTest{ + Tool: taskutil.Tool{ + RunArgs: []string{"go", "test", "./..."}, + }, + }, + } + } + + return nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t goModCheck) InfoText() string { return "go.mod tidy check" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t goModCheck) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t goModCheck) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t goFormat) InfoText() string { return "Format" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t goFormat) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t goFormat) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t generateCodeCI) InfoText() string { return "Generate code" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t generateCodeCI) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t generateCodeCI) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t goBuildCI) InfoText() string { return "Build" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t goBuildCI) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t goBuildCI) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t goVet) InfoText() string { return "Vet" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t goVet) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t goVet) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t staticcheck) InfoText() string { return "Lint (staticcheck)" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t staticcheck) Exec(ctx context.Context) error { + if err := toolcfg.SetupConfigFile(t.Tool); err != nil { + return err + } + + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t staticcheck) Post(_ context.Context) error { + if err := os.RemoveAll(t.ConfigFilePath); err != nil { + return fmt.Errorf("removing config file: %w", err) + } + + return nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t revive) InfoText() string { return "Lint (revive)" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t revive) Exec(ctx context.Context) error { + if err := toolcfg.SetupConfigFile(t.Tool); err != nil { + return err + } + + if _, err := taskutil.RunCommand(ctx, t.RenderRunCommandArgs()); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t revive) Post(_ context.Context) error { + if err := os.RemoveAll(t.ConfigFilePath); err != nil { + return fmt.Errorf("removing config file: %w", err) + } + + return nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t errcheck) InfoText() string { return "Lint (errcheck)" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t errcheck) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t errcheck) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t goImports) InfoText() string { return "Format imports" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t goImports) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t goImports) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t govulncheck) InfoText() string { return "Vulnerability scan (govulncheck)" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t govulncheck) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t govulncheck) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t goTest) InfoText() string { return "Tests" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t goTest) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t goTest) Post(_ context.Context) error { return nil } diff --git a/internal/tools/go/deliver.go b/internal/tasks/tools/go/deliver.go similarity index 71% rename from internal/tools/go/deliver.go rename to internal/tasks/tools/go/deliver.go index 28c44ea..e8da680 100644 --- a/internal/tools/go/deliver.go +++ b/internal/tasks/tools/go/deliver.go @@ -7,31 +7,31 @@ import ( "path/filepath" "strings" - "github.com/opensourcecorp/oscar/internal/tools" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) type ( - ghRelease struct{} + ghRelease struct{ taskutil.Tool } ) -var deliveryTasks = []tools.Tasker{ - ghRelease{}, -} - // TasksForDelivery returns the list of Delivery tasks. -func TasksForDelivery(repo tools.Repo) []tools.Tasker { +func TasksForDelivery(repo taskutil.Repo) []taskutil.Tasker { if repo.HasGo { - return deliveryTasks + return []taskutil.Tasker{ + ghRelease{ + Tool: taskutil.Tool{}, + }, + } } return nil } -// InfoText implements [tools.Tasker.InfoText]. +// InfoText implements [taskutil.Tasker.InfoText]. func (t ghRelease) InfoText() string { return "GitHub Release" } -// Run implements [tools.Tasker.Run]. -func (t ghRelease) Run(ctx context.Context) error { +// Exec implements [taskutil.Tasker.Exec]. +func (t ghRelease) Exec(ctx context.Context) error { targetDir := "build" if err := os.RemoveAll(targetDir); err != nil { @@ -59,7 +59,7 @@ func (t ghRelease) Run(ctx context.Context) error { src := "./cmd/oscar" target := filepath.Join(targetDir, fmt.Sprintf("%s-%s-%s", binName, goos, goarch)) - if _, err := tools.RunCommand(ctx, []string{"bash", "-c", fmt.Sprintf(` + if _, err := taskutil.RunCommand(ctx, []string{"bash", "-c", fmt.Sprintf(` CGO_ENABLED=0 \ GOOS=%s GOARCH=%s \ go build -ldflags '-extldflags "-static"' -o %s %s`, @@ -77,5 +77,5 @@ func (t ghRelease) Run(ctx context.Context) error { return nil } -// Post implements [tools.Tasker.Post]. +// Post implements [taskutil.Tasker.Post]. func (t ghRelease) Post(_ context.Context) error { return nil } diff --git a/internal/tools/go/doc.go b/internal/tasks/tools/go/doc.go similarity index 100% rename from internal/tools/go/doc.go rename to internal/tasks/tools/go/doc.go diff --git a/internal/tasks/tools/markdown/ci.go b/internal/tasks/tools/markdown/ci.go new file mode 100644 index 0000000..abc3938 --- /dev/null +++ b/internal/tasks/tools/markdown/ci.go @@ -0,0 +1,49 @@ +package mdtools + +import ( + "context" + "os" + "path/filepath" + + "github.com/opensourcecorp/oscar/internal/tasks/tools/toolcfg" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" +) + +type ( + markdownlint struct{ taskutil.Tool } +) + +// NewTasksForCI returns the list of CI tasks. +func NewTasksForCI(repo taskutil.Repo) []taskutil.Tasker { + if repo.HasMarkdown { + return []taskutil.Tasker{ + markdownlint{ + Tool: taskutil.Tool{ + RunArgs: []string{"markdownlint-cli2", "--config", "{{ConfigFilePath}}", "**/*.md"}, + ConfigFilePath: filepath.Join(os.TempDir(), ".markdownlint-cli2.yaml"), + }, + }, + } + } + + return nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t markdownlint) InfoText() string { return "Lint (markdownlint)" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t markdownlint) Exec(ctx context.Context) error { + if err := toolcfg.SetupConfigFile(t.Tool); err != nil { + return err + } + + if _, err := taskutil.RunCommand(ctx, t.RenderRunCommandArgs()); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t markdownlint) Post(_ context.Context) error { return nil } diff --git a/internal/tools/markdown/doc.go b/internal/tasks/tools/markdown/doc.go similarity index 100% rename from internal/tools/markdown/doc.go rename to internal/tasks/tools/markdown/doc.go diff --git a/internal/tasks/tools/python/ci.go b/internal/tasks/tools/python/ci.go new file mode 100644 index 0000000..a53e15f --- /dev/null +++ b/internal/tasks/tools/python/ci.go @@ -0,0 +1,125 @@ +package pytools + +import ( + "context" + + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" +) + +type ( + buildTask struct{ taskutil.Tool } + ruffLint struct{ taskutil.Tool } + ruffFormat struct{ taskutil.Tool } + pydoclint struct{ taskutil.Tool } + mypy struct{ taskutil.Tool } +) + +// NewTasksForCI returns the list of CI tasks. +func NewTasksForCI(repo taskutil.Repo) []taskutil.Tasker { + if repo.HasPython { + return []taskutil.Tasker{ + buildTask{ + Tool: taskutil.Tool{ + RunArgs: []string{"uv", "build"}, + }, + }, + ruffLint{ + Tool: taskutil.Tool{ + RunArgs: []string{"ruff", "check", "--fix", "./src"}, + }, + }, + ruffFormat{ + Tool: taskutil.Tool{ + RunArgs: []string{"ruff", "format", "./src"}, + }, + }, + pydoclint{ + Tool: taskutil.Tool{ + RunArgs: []string{"uvx", "pydoclint", "./src"}, + }, + }, + mypy{ + Tool: taskutil.Tool{ + RunArgs: []string{"uvx", "mypy", "./src"}, + }, + }, + } + } + + return nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t buildTask) InfoText() string { return "Build" } + +// Run implements [taskutil.Tasker.Run]. +func (t buildTask) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t buildTask) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t ruffLint) InfoText() string { return "Lint (ruff)" } + +// Run implements [taskutil.Tasker.Run]. +func (t ruffLint) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t ruffLint) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t ruffFormat) InfoText() string { return "Format (ruff)" } + +// Run implements [taskutil.Tasker.Run]. +func (t ruffFormat) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t ruffFormat) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t pydoclint) InfoText() string { return "Lint (pydoclint)" } + +// Run implements [taskutil.Tasker.Run]. +func (t pydoclint) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t pydoclint) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t mypy) InfoText() string { return "Type-check (mypy)" } + +// Run implements [taskutil.Tasker.Run]. +func (t mypy) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t mypy) Post(_ context.Context) error { return nil } diff --git a/internal/tools/python/doc.go b/internal/tasks/tools/python/doc.go similarity index 100% rename from internal/tools/python/doc.go rename to internal/tasks/tools/python/doc.go diff --git a/internal/tasks/tools/shell/ci.go b/internal/tasks/tools/shell/ci.go new file mode 100644 index 0000000..fa2d5d7 --- /dev/null +++ b/internal/tasks/tools/shell/ci.go @@ -0,0 +1,70 @@ +package shtools + +import ( + "context" + + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" +) + +type ( + shellcheck struct{ taskutil.Tool } + shfmt struct{ taskutil.Tool } +) + +// NewTasksForCI returns the list of CI tasks. +func NewTasksForCI(repo taskutil.Repo) []taskutil.Tasker { + if repo.HasShell { + return []taskutil.Tasker{ + shellcheck{ + Tool: taskutil.Tool{ + RunArgs: []string{"bash", "-c", ` + shopt -s globstar + ls **/*.sh || exit 0 + shellcheck **/*.sh`, + }, + }, + }, + shfmt{ + Tool: taskutil.Tool{ + RunArgs: []string{"bash", "-c", ` + shopt -s globstar + ls **/*.sh || exit 0 + shfmt **/*.sh`, + }, + }, + }, + } + } + + return nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t shellcheck) InfoText() string { return "Lint (shellcheck)" } + +// Run implements [taskutil.Tasker.Run]. +func (t shellcheck) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t shellcheck) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t shfmt) InfoText() string { return "Format (shfmt)" } + +// Run implements [taskutil.Tasker.Run]. +func (t shfmt) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t shfmt) Post(_ context.Context) error { return nil } diff --git a/internal/tools/shell/doc.go b/internal/tasks/tools/shell/doc.go similarity index 100% rename from internal/tools/shell/doc.go rename to internal/tasks/tools/shell/doc.go diff --git a/internal/tools/toolcfg/.markdownlint-cli2.yaml b/internal/tasks/tools/toolcfg/.markdownlint-cli2.yaml similarity index 100% rename from internal/tools/toolcfg/.markdownlint-cli2.yaml rename to internal/tasks/tools/toolcfg/.markdownlint-cli2.yaml diff --git a/internal/tasks/tools/toolcfg/.yamlfmt b/internal/tasks/tools/toolcfg/.yamlfmt new file mode 100644 index 0000000..481d5b2 --- /dev/null +++ b/internal/tasks/tools/toolcfg/.yamlfmt @@ -0,0 +1,8 @@ +--- + +# Rules found at: + +formatter: + type: "basic" + line_ending: "lf" + include_document_start: true diff --git a/internal/tools/toolcfg/.yamllint b/internal/tasks/tools/toolcfg/.yamllint similarity index 76% rename from internal/tools/toolcfg/.yamllint rename to internal/tasks/tools/toolcfg/.yamllint index 5ebd35e..c344ea6 100644 --- a/internal/tools/toolcfg/.yamllint +++ b/internal/tasks/tools/toolcfg/.yamllint @@ -1,5 +1,6 @@ --- extends: "default" +# Rules found at: https://yamllint.readthedocs.io/en/stable/rules.html rules: line-length: max: 100 diff --git a/internal/tools/toolcfg/embed.go b/internal/tasks/tools/toolcfg/doc.go similarity index 56% rename from internal/tools/toolcfg/embed.go rename to internal/tasks/tools/toolcfg/doc.go index 7e4547a..e7bd20e 100644 --- a/internal/tools/toolcfg/embed.go +++ b/internal/tasks/tools/toolcfg/doc.go @@ -1,10 +1,3 @@ // Package toolcfg is used for storing embeddable config files for various tools, that are injected // at runtime. package toolcfg - -import "embed" - -// Files stores config files for each CI tool. -// -//go:embed * -var Files embed.FS diff --git a/internal/tasks/tools/toolcfg/embed.go b/internal/tasks/tools/toolcfg/embed.go new file mode 100644 index 0000000..3641dd9 --- /dev/null +++ b/internal/tasks/tools/toolcfg/embed.go @@ -0,0 +1,8 @@ +package toolcfg + +import "embed" + +// Files stores config files for each CI tool. +// +//go:embed * +var Files embed.FS diff --git a/internal/tasks/tools/toolcfg/hadolint.yaml b/internal/tasks/tools/toolcfg/hadolint.yaml new file mode 100644 index 0000000..f6cc8f3 --- /dev/null +++ b/internal/tasks/tools/toolcfg/hadolint.yaml @@ -0,0 +1,9 @@ +--- +failure-threshold: "style" +# Rules found at: https://github.com/hadolint/hadolint?tab=readme-ov-file#rules +ignored: + - "DL3008" + - "DL3018" + - "DL3033" + - "DL3037" + - "DL3041" diff --git a/internal/tools/toolcfg/pyproject.toml b/internal/tasks/tools/toolcfg/pyproject.toml similarity index 100% rename from internal/tools/toolcfg/pyproject.toml rename to internal/tasks/tools/toolcfg/pyproject.toml diff --git a/internal/tools/toolcfg/revive.toml b/internal/tasks/tools/toolcfg/revive.toml similarity index 100% rename from internal/tools/toolcfg/revive.toml rename to internal/tasks/tools/toolcfg/revive.toml diff --git a/internal/tools/toolcfg/staticcheck.conf b/internal/tasks/tools/toolcfg/staticcheck.conf similarity index 100% rename from internal/tools/toolcfg/staticcheck.conf rename to internal/tasks/tools/toolcfg/staticcheck.conf diff --git a/internal/tasks/tools/toolcfg/util.go b/internal/tasks/tools/toolcfg/util.go new file mode 100644 index 0000000..a04217f --- /dev/null +++ b/internal/tasks/tools/toolcfg/util.go @@ -0,0 +1,24 @@ +package toolcfg + +import ( + "fmt" + "os" + "path/filepath" + + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" +) + +// SetupConfigFile handles reading a Tool's config file from the embedded filesystem, and writing it +// to its target location. +func SetupConfigFile(t taskutil.Tool) error { + cfgFileContents, err := Files.ReadFile(filepath.Base(t.ConfigFilePath)) + if err != nil { + return fmt.Errorf("reading embedded file contents: %w", err) + } + + if err := os.WriteFile(t.ConfigFilePath, cfgFileContents, 0644); err != nil { + return fmt.Errorf("writing config file: %w", err) + } + + return nil +} diff --git a/internal/tools/version/version.go b/internal/tasks/tools/version/ci.go similarity index 67% rename from internal/tools/version/version.go rename to internal/tasks/tools/version/ci.go index 6e52efe..d064af6 100644 --- a/internal/tools/version/version.go +++ b/internal/tasks/tools/version/ci.go @@ -6,30 +6,29 @@ import ( "fmt" "os" "path/filepath" + "regexp" "github.com/opensourcecorp/oscar/internal/consts" "github.com/opensourcecorp/oscar/internal/oscarcfg" iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/semver" - "github.com/opensourcecorp/oscar/internal/tools" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) -type versionCI struct{} +type versionCI struct{ taskutil.Tool } -var tasks = []tools.Tasker{ - versionCI{}, -} - -// TasksForCI returns the list of CI tasks. -func TasksForCI(_ tools.Repo) []tools.Tasker { - return tasks +// NewTasksForCI returns the list of CI tasks. +func NewTasksForCI(_ taskutil.Repo) []taskutil.Tasker { + return []taskutil.Tasker{ + versionCI{}, + } } -// InfoText implements [tools.Tasker.InfoText]. +// InfoText implements [taskutil.Tasker.InfoText]. func (t versionCI) InfoText() string { return "Versioning checks" } -// Run implements [tools.Tasker.Run]. -func (t versionCI) Run(ctx context.Context) (err error) { +// Exec implements [taskutil.Tasker.Exec]. +func (t versionCI) Exec(ctx context.Context) (err error) { cfg, err := oscarcfg.Get() if err != nil { return fmt.Errorf("getting oscar config: %w", err) @@ -51,12 +50,14 @@ func (t versionCI) Run(ctx context.Context) (err error) { } }() - remote, err := tools.RunCommand(ctx, []string{"git", "remote", "get-url", "origin"}) + remote, err := taskutil.RunCommand(ctx, []string{"git", "remote", "get-url", "origin"}) if err != nil { return fmt.Errorf("determining git root: %w", err) } - if _, err := tools.RunCommand(ctx, []string{"git", "clone", "--depth", "1", remote, tmpCloneDir}); err != nil { + remote = canonicalizeGitRemote(remote) + + if _, err := taskutil.RunCommand(ctx, []string{"git", "clone", "--depth", "1", remote, tmpCloneDir}); err != nil { return fmt.Errorf("cloning repo source to temp location: %w", err) } @@ -72,7 +73,7 @@ func (t versionCI) Run(ctx context.Context) (err error) { // // TODO: update internal git package to have a type with ALL this info so I stop copy-pasting // shell-outs around - branch, err := tools.RunCommand(ctx, []string{"git", "rev-parse", "--abbrev-ref", "HEAD"}) + branch, err := taskutil.RunCommand(ctx, []string{"git", "rev-parse", "--abbrev-ref", "HEAD"}) if err != nil { return fmt.Errorf("checking current Git branch/ref: %w", err) } @@ -90,5 +91,15 @@ func (t versionCI) Run(ctx context.Context) (err error) { return nil } -// Post implements [tools.Tasker.Post]. +// Post implements [taskutil.Tasker.Post]. func (t versionCI) Post(_ context.Context) error { return nil } + +// canonicalizeGitRemote converts a Git remote string to be in canonical HTTPS format. +func canonicalizeGitRemote(remote string) string { + gitSSHRemoteRegex := regexp.MustCompile(`^(https://|git@)(.*)(:|/)(.*)/(.*(.git)?)$`) + groups := gitSSHRemoteRegex.FindStringSubmatch(remote) + + out := fmt.Sprintf("https://%s/%s/%s", groups[2], groups[4], groups[5]) + + return out +} diff --git a/internal/tasks/tools/version/ci_test.go b/internal/tasks/tools/version/ci_test.go new file mode 100644 index 0000000..536cd0e --- /dev/null +++ b/internal/tasks/tools/version/ci_test.go @@ -0,0 +1,13 @@ +package versiontools + +import "testing" + +func TestCanonicalizeGitRemote(t *testing.T) { + remote := "git@github.com:opensourcecorp/oscar.git" + want := "https://github.com/opensourcecorp/oscar.git" + got := canonicalizeGitRemote(remote) + + if want != got { + t.Errorf("\nwant: %v\ngot: %v", want, got) + } +} diff --git a/internal/tools/version/doc.go b/internal/tasks/tools/version/doc.go similarity index 100% rename from internal/tools/version/doc.go rename to internal/tasks/tools/version/doc.go diff --git a/internal/tasks/tools/yaml/ci.go b/internal/tasks/tools/yaml/ci.go new file mode 100644 index 0000000..6f8b3ea --- /dev/null +++ b/internal/tasks/tools/yaml/ci.go @@ -0,0 +1,86 @@ +package yamltools + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/opensourcecorp/oscar/internal/tasks/tools/toolcfg" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" +) + +type ( + yamlfmt struct{ taskutil.Tool } + yamllint struct{ taskutil.Tool } +) + +// NewTasksForCI returns the list of CI tasks. +func NewTasksForCI(repo taskutil.Repo) []taskutil.Tasker { + if repo.HasYaml { + return []taskutil.Tasker{ + yamlfmt{ + Tool: taskutil.Tool{ + RunArgs: []string{"bash", "-c", + fmt.Sprintf( + `yamlfmt -conf {{ConfigFilePath}} $(%s)`, + taskutil.GetFileTypeListerCommand("yaml"), + ), + }, + ConfigFilePath: filepath.Join(os.TempDir(), ".yamlfmt"), + }, + }, + yamllint{ + Tool: taskutil.Tool{ + RunArgs: []string{"bash", "-c", + fmt.Sprintf( + `yamllint --strict --config-file {{ConfigFilePath}} $(%s)`, + taskutil.GetFileTypeListerCommand("yaml"), + ), + }, + ConfigFilePath: filepath.Join(os.TempDir(), ".yamllint"), + }, + }, + } + } + + return nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t yamllint) InfoText() string { return "Lint (yamllint)" } + +// Run implements [taskutil.Tasker.Run]. +func (t yamllint) Exec(ctx context.Context) error { + if err := toolcfg.SetupConfigFile(t.Tool); err != nil { + return err + } + + if _, err := taskutil.RunCommand(ctx, t.RenderRunCommandArgs()); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t yamllint) Post(_ context.Context) error { return nil } + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t yamlfmt) InfoText() string { return "Format (yamlfmt)" } + +// Run implements [taskutil.Tasker.Run]. +func (t yamlfmt) Exec(ctx context.Context) error { + if err := toolcfg.SetupConfigFile(t.Tool); err != nil { + return err + } + + if _, err := taskutil.RunCommand(ctx, t.RenderRunCommandArgs()); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t yamlfmt) Post(_ context.Context) error { return nil } diff --git a/internal/tools/yaml/doc.go b/internal/tasks/tools/yaml/doc.go similarity index 100% rename from internal/tools/yaml/doc.go rename to internal/tasks/tools/yaml/doc.go diff --git a/internal/tasks/util/doc.go b/internal/tasks/util/doc.go new file mode 100644 index 0000000..083cc6d --- /dev/null +++ b/internal/tasks/util/doc.go @@ -0,0 +1,3 @@ +// Package taskutil contains type definitions and helper functions for working with Tasks & Tools +// across the oscar codebase. +package taskutil diff --git a/internal/tools/types.go b/internal/tasks/util/types.go similarity index 53% rename from internal/tools/types.go rename to internal/tasks/util/types.go index cb61bc4..99caa1f 100644 --- a/internal/tools/types.go +++ b/internal/tasks/util/types.go @@ -1,57 +1,65 @@ -package tools +package taskutil -import "context" +import ( + "context" + "time" +) // Tasker defines the method set for working with metadata for a given CI Task. type Tasker interface { // InfoText should return a human-readable display string that describes the task, e.g. "Run - // tests". If this is unset, then its banner will not show in the CI log output at all (which - // may be desirable) in the case of implementers of [Tasker.Init]) + // tests". InfoText() string - // Run should perform the actual task's actions. - Run(ctx context.Context) error + // Exec should perform the actual task's actions. + Exec(ctx context.Context) error // Post should perform any post-run actions for the task, if necessary. Post(ctx context.Context) error } -// Repo stores information about the contents of the repository being ran against. -type Repo struct { - HasGo bool - HasPython bool - HasShell bool - HasTerraform bool - HasYaml bool - HasMarkdown bool -} - -// TaskMap is a less-verbose type alias for mapping language names to function signatures that -// return a language's tasks. -type TaskMap map[string][]Tasker +// TasksMap aliases a map of a Task's language/tooling name to its list of Tasks. +type TasksMap map[string][]Tasker // A Tool defines information about a tool used for running oscar's tasks. A Tool should be defined // if a language etc. cannot perform the task itself. For example, you would not need a Tool to // represent a task that runs "go test", but you *would* need a tool to represent a task that runs // the external "staticcheck" linter for Go. -// -// Every Tool should implement [Tasker]. type Tool struct { - // The tool's name, used as an identifier. May also be the tool's invocable command, in which - // case it can be interpolated as such. - Name string + // TODO + RunArgs []string // The path to the tool's config file, if it has one to use. ConfigFilePath string - // The optional installable path for the tool, like a URL. Can also be a format string, e.g. - // with placeholders for platform-specific strings. Should mostly not be needed if using mise. - RemotePath string - // The version of the tool. Should mostly not be needed if using mise. - Version string +} + +// A Run holds metadata about an instance of an oscar subcommand run, e.g. a list of Tasks for the +// `ci` subcommand. +type Run struct { + // TasksMap TasksMap + // A timestamp for storing when the overall run started. + StartTime time.Time + // The length of the longest language/tooling name string. + LongestLanguageNameLength int + // The length of the longest string output by [Tasker.InfoText]. + LongestInfoTextLength int + // Keeps track of all task failures. + Failures []string +} + +// A Repo stores information about the contents of the repository being run against. +type Repo struct { + HasGo bool + HasPython bool + HasShell bool + HasTerraform bool + HasContainerfile bool + HasYaml bool + HasMarkdown bool } // String implements the [fmt.Stringer] interface. func (repo Repo) String() string { var out string - out += "The following file types were found in this repo, and tasks can/will be run against them:\n" + out += "The following file types were found in this repo, and tasks will be run against them:\n" if repo.HasGo { out += "- Go\n" @@ -65,6 +73,9 @@ func (repo Repo) String() string { if repo.HasTerraform { out += "- Terraform\n" } + if repo.HasContainerfile { + out += "- Containerfile\n" + } if repo.HasYaml { out += "- YAML\n" } diff --git a/internal/tools/util.go b/internal/tasks/util/util.go similarity index 66% rename from internal/tools/util.go rename to internal/tasks/util/util.go index c9a758a..9941a05 100644 --- a/internal/tools/util.go +++ b/internal/tasks/util/util.go @@ -1,4 +1,4 @@ -package tools +package taskutil import ( "context" @@ -82,9 +82,39 @@ func InitSystem(ctx context.Context) error { return nil } -// RunCommand takes a string slice containing an entire command & its args to run, and returns a -// consistent error message in case of failure. It also returns the command output, in case the -// caller needs to parse it on their own. +// NewRun returns a populated [Run]. +func NewRun(ctx context.Context, tasksMap TasksMap) (Run, error) { + // Handle system init + if err := InitSystem(ctx); err != nil { + return Run{}, fmt.Errorf("initializing system: %w", err) + } + + // Vars for determining text padding in output banners + var ( + longestLanguageNameLength int + longestInfoTextLength int + ) + for lang, taskMap := range tasksMap { + longestLanguageNameLength = max(longestLanguageNameLength, len(lang)) + for _, task := range taskMap { + longestInfoTextLength = max(longestInfoTextLength, len(task.InfoText())) + } + } + iprint.Debugf("longestLanguageNameLength: %d\n", longestLanguageNameLength) + iprint.Debugf("longestInfoTextLength: %d\n", longestInfoTextLength) + + return Run{ + // TasksMap: tasksMap, + StartTime: time.Now(), + LongestLanguageNameLength: longestLanguageNameLength, + LongestInfoTextLength: longestInfoTextLength, + Failures: make([]string, 0), + }, nil +} + +// RunCommand takes a string slice containing a command & its args to run, and returns a consistent +// error message in case of failure. It also returns the command output, in case the caller needs to +// parse it on their own. func RunCommand(ctx context.Context, cmdArgs []string) (string, error) { if len(cmdArgs) <= 1 { return "", fmt.Errorf("internal error: not enough arguments passed to RunCommand() -- received: %v", cmdArgs) @@ -124,12 +154,17 @@ func GetRepoComposition(ctx context.Context) (Repo, error) { errs = errors.Join(errs, err) } + hasShell, err := filesExistInTree(ctx, GetFileTypeListerCommand("sh")) + if err != nil { + errs = errors.Join(errs, err) + } + hasTerraform, err := filesExistInTree(ctx, GetFileTypeListerCommand("tf")) if err != nil { errs = errors.Join(errs, err) } - hasShell, err := filesExistInTree(ctx, GetFileTypeListerCommand("sh")) + hasContainerfile, err := filesExistInTree(ctx, GetFileTypeListerCommand("containerfile")) if err != nil { errs = errors.Join(errs, err) } @@ -149,31 +184,65 @@ func GetRepoComposition(ctx context.Context) (Repo, error) { } repo := Repo{ - HasGo: hasGo, - HasPython: hasPython, - HasShell: hasShell, - HasTerraform: hasTerraform, - HasYaml: hasYaml, - HasMarkdown: hasMarkdown, + HasGo: hasGo, + HasPython: hasPython, + HasShell: hasShell, + HasTerraform: hasTerraform, + HasContainerfile: hasContainerfile, + HasYaml: hasYaml, + HasMarkdown: hasMarkdown, } iprint.Debugf("repo composition: %+v\n", repo) return repo, nil } -// ripgrep file-type spec. Used because it supports gitignoreables +// GetFileTypeListerCommand takes a [ripgrep]-known file type, and returns a slice containing a +// command & its args to find matching files. ripgrep is used as the default file-finder across the +// codebase because not only is it fast, but it also supports files like `.gitignore` without any +// extra configuration. +// +// [ripgrep]: https://github.com/BurntSushi/ripgrep func GetFileTypeListerCommand(fileType string) string { - return fmt.Sprintf(`rg --files --type '%s' || true`, fileType) + // NOTE: there are some special cases we need to handle, like how ripgrep understands "docker" + // as a file type arg (and it will find files matching the glob "*Dockerfile*"), but it will + // *not* find e.g. "Containerfile" + switch fileType { + case "containerfile": + return `rg --files --glob-case-insensitive --glob='*{Containerfile,Dockerfile}*' || true` + default: + return fmt.Sprintf(`rg --files --type '%s' || true`, fileType) + } } -// RunDurationString returns a calculated duration used to indicate how long a particular task took -// to run. +// RunDurationString returns a calculated duration used to indicate how long a particular Task (or +// set of Tasks) took to run. func RunDurationString(t time.Time) string { return fmt.Sprintf("t: %s", time.Since(t).Round(time.Second/1000).String()) } -// installMise determines if mise needs to be installed on the host, and if so, installs it into -// [consts.OscarHomeBin]. +// RenderRunCommandArgs uses [Tool.RunArgs] and does naive templating to replace certain values +// before being used. +// +// This is useful because when instantiating [Tasker]s, sometimes the fields need to be +// self-referential within the struct. For example, if a [Tool.RunArgs] needs to specify a config +// file path, but it can't know that value until instantiation (even though it's likely defined +// right below it in [Tool.ConfigFilePath]), you can write the `RunArgs` to have a +// `{{ConfigFilePath}}` placeholder that will be interpolated when calling this function. +func (t Tool) RenderRunCommandArgs() []string { + out := make([]string, len(t.RunArgs)) + for i, arg := range t.RunArgs { + out[i] = strings.ReplaceAll(arg, `{{ConfigFilePath}}`, t.ConfigFilePath) + } + + iprint.Debugf("RenderRunCommandArgs: %#v\n", out) + + return out +} + +// installMise installs [mise] into [consts.OscarHomeBin], if not found there. +// +// [mise]: https://mise.jdx.dev func installMise(_ context.Context) (err error) { miseFound := true _, err = os.Stat(consts.MiseBinPath) @@ -188,6 +257,7 @@ func installMise(_ context.Context) (err error) { } if miseFound { + // TODO: mise version check iprint.Debugf("mise found, nothing to do\n") return } diff --git a/internal/tools/doc.go b/internal/tools/doc.go deleted file mode 100644 index a24f8dd..0000000 --- a/internal/tools/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package tools contains common functionality for any tool usage. -package tools diff --git a/internal/tools/go/ci.go b/internal/tools/go/ci.go deleted file mode 100644 index bb92f4d..0000000 --- a/internal/tools/go/ci.go +++ /dev/null @@ -1,271 +0,0 @@ -package gotools - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/opensourcecorp/oscar/internal/tools" - "github.com/opensourcecorp/oscar/internal/tools/toolcfg" -) - -type ( - goModCheckCI struct{} - goFormatCI struct{} - generateCodeCI struct{} - goBuildCI struct{} - goVetCI struct{} - staticcheckCI struct{} - reviveCI struct{} - errcheckCI struct{} - goImportsCI struct{} - govulncheckCI struct{} - goTestCI struct{} -) - -var ciTasks = []tools.Tasker{ - goModCheckCI{}, - goFormatCI{}, - generateCodeCI{}, - goBuildCI{}, - goVetCI{}, - staticcheckCI{}, - reviveCI{}, - errcheckCI{}, - goImportsCI{}, - govulncheckCI{}, - goTestCI{}, -} - -var ( - staticcheck = tools.Tool{ - Name: "staticcheck", - ConfigFilePath: filepath.Join("./staticcheck.conf"), - } - revive = tools.Tool{ - Name: "revive", - ConfigFilePath: filepath.Join(os.TempDir(), "revive.toml"), - } - errcheck = tools.Tool{ - Name: "errcheck", - } - goimports = tools.Tool{ - Name: "goimports", - } - govulncheck = tools.Tool{ - Name: "govulncheck", - } -) - -// TasksForCI returns the list of CI tasks. -func TasksForCI(repo tools.Repo) []tools.Tasker { - if repo.HasGo { - return ciTasks - } - - return nil -} - -// InfoText implements [tools.Tasker.InfoText]. -func (t goModCheckCI) InfoText() string { return "go.mod tidy check" } - -// Run implements [tools.Tasker.Run]. -func (t goModCheckCI) Run(ctx context.Context) error { - if _, err := tools.RunCommand(ctx, []string{"go", "mod", "tidy"}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t goModCheckCI) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t goFormatCI) InfoText() string { return "Format" } - -// Run implements [tools.Tasker.Run]. -func (t goFormatCI) Run(ctx context.Context) error { - if _, err := tools.RunCommand(ctx, []string{"go", "fmt", "./..."}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t goFormatCI) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t generateCodeCI) InfoText() string { return "Generate code" } - -// Run implements [tools.Tasker.Run]. -func (t generateCodeCI) Run(ctx context.Context) error { - if _, err := tools.RunCommand(ctx, []string{"go", "generate", "./..."}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t generateCodeCI) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t goBuildCI) InfoText() string { return "Build" } - -// Run implements [tools.Tasker.Run]. -func (t goBuildCI) Run(ctx context.Context) error { - if _, err := tools.RunCommand(ctx, []string{"go", "build", "./..."}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t goBuildCI) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t goVetCI) InfoText() string { return "Vet" } - -// Run implements [tools.Tasker.Run]. -func (t goVetCI) Run(ctx context.Context) error { - if _, err := tools.RunCommand(ctx, []string{"go", "vet", "./..."}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t goVetCI) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t staticcheckCI) InfoText() string { return "Lint (staticcheck)" } - -// Run implements [tools.Tasker.Run]. -func (t staticcheckCI) Run(ctx context.Context) (err error) { - cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(staticcheck.ConfigFilePath)) - if err != nil { - return fmt.Errorf("reading embedded file contents: %w", err) - } - - if err := os.WriteFile(staticcheck.ConfigFilePath, cfgFileContents, 0644); err != nil { - return fmt.Errorf("writing config file: %w", err) - } - - if _, err := tools.RunCommand(ctx, []string{staticcheck.Name, "./..."}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t staticcheckCI) Post(_ context.Context) error { - if err := os.RemoveAll(staticcheck.ConfigFilePath); err != nil { - return fmt.Errorf("removing config file: %w", err) - } - - return nil -} - -// InfoText implements [tools.Tasker.InfoText]. -func (t reviveCI) InfoText() string { return "Lint (revive)" } - -// Run implements [tools.Tasker.Run]. -func (t reviveCI) Run(ctx context.Context) error { - cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(revive.ConfigFilePath)) - if err != nil { - return fmt.Errorf("reading embedded file contents: %w", err) - } - - if err := os.WriteFile(revive.ConfigFilePath, cfgFileContents, 0644); err != nil { - return fmt.Errorf("writing config file: %w", err) - } - - args := []string{ - revive.Name, - "--config", revive.ConfigFilePath, - "--set_exit_status", - "./...", - } - - if _, err := tools.RunCommand(ctx, args); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t reviveCI) Post(_ context.Context) error { - if err := os.RemoveAll(revive.ConfigFilePath); err != nil { - return fmt.Errorf("removing config file: %w", err) - } - - return nil -} - -// InfoText implements [tools.Tasker.InfoText]. -func (t errcheckCI) InfoText() string { return "Lint (errcheck)" } - -// Run implements [tools.Tasker.Run]. -func (t errcheckCI) Run(ctx context.Context) error { - if _, err := tools.RunCommand(ctx, []string{errcheck.Name, "./..."}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t errcheckCI) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t goImportsCI) InfoText() string { return "Format imports" } - -// Run implements [tools.Tasker.Run]. -func (t goImportsCI) Run(ctx context.Context) error { - args := []string{goimports.Name, "-l", "-w", "."} - if _, err := tools.RunCommand(ctx, args); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t goImportsCI) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t govulncheckCI) InfoText() string { return "Vulnerability scan (govulncheck)" } - -// Run implements [tools.Tasker.Run]. -func (t govulncheckCI) Run(ctx context.Context) error { - if _, err := tools.RunCommand(ctx, []string{govulncheck.Name, "./..."}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t govulncheckCI) Post(ctx context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t goTestCI) InfoText() string { return "Test" } - -// Run implements [tools.Tasker.Run]. -func (t goTestCI) Run(ctx context.Context) error { - if _, err := tools.RunCommand(ctx, []string{"go", "test", "./..."}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t goTestCI) Post(ctx context.Context) error { return nil } diff --git a/internal/tools/markdown/tasks.go b/internal/tools/markdown/tasks.go deleted file mode 100644 index fbe9d20..0000000 --- a/internal/tools/markdown/tasks.go +++ /dev/null @@ -1,58 +0,0 @@ -package mdtools - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/opensourcecorp/oscar/internal/tools" - "github.com/opensourcecorp/oscar/internal/tools/toolcfg" -) - -type ( - markdownlintTask struct{} -) - -var tasks = []tools.Tasker{ - markdownlintTask{}, -} - -// Tasks returns the list of CI tasks. -func Tasks(repo tools.Repo) []tools.Tasker { - if repo.HasMarkdown { - return tasks - } - - return nil -} - -// InfoText implements [tools.Tasker.InfoText]. -func (t markdownlintTask) InfoText() string { return "Lint (markdownlint)" } - -// Run implements [tools.Tasker.Run]. -func (t markdownlintTask) Run(ctx context.Context) error { - cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(markdownlint.ConfigFilePath)) - if err != nil { - return fmt.Errorf("reading embedded file contents: %w", err) - } - - if err := os.WriteFile(markdownlint.ConfigFilePath, cfgFileContents, 0644); err != nil { - return fmt.Errorf("writing config file: %w", err) - } - - args := []string{ - markdownlint.Name, - "--config", markdownlint.ConfigFilePath, - "**/*.md", - } - - if _, err := tools.RunCommand(ctx, args); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t markdownlintTask) Post(_ context.Context) error { return nil } diff --git a/internal/tools/markdown/versions.go b/internal/tools/markdown/versions.go deleted file mode 100644 index 9f83cbc..0000000 --- a/internal/tools/markdown/versions.go +++ /dev/null @@ -1,15 +0,0 @@ -package mdtools - -import ( - "os" - "path/filepath" - - "github.com/opensourcecorp/oscar/internal/tools" -) - -var ( - markdownlint = tools.Tool{ - Name: "markdownlint-cli2", - ConfigFilePath: filepath.Join(os.TempDir(), ".markdownlint-cli2.yaml"), - } -) diff --git a/internal/tools/python/tasks.go b/internal/tools/python/tasks.go deleted file mode 100644 index 046a19e..0000000 --- a/internal/tools/python/tasks.go +++ /dev/null @@ -1,136 +0,0 @@ -package pytools - -import ( - "context" - "fmt" - "slices" - - "github.com/opensourcecorp/oscar/internal/tools" -) - -type ( - baseConfigTask struct{} - buildTask struct{} - ruffLintTask struct{} - ruffFormatTask struct{} - pydoclintTask struct{} - mypyTask struct{} -) - -var tasks = []tools.Tasker{ - baseConfigTask{}, - buildTask{}, - ruffLintTask{}, - ruffFormatTask{}, - pydoclintTask{}, - mypyTask{}, -} - -// Tasks returns the list of CI tasks. -func Tasks(repo tools.Repo) []tools.Tasker { - if repo.HasPython { - return tasks - } - - return nil -} - -// InfoText implements [tools.Tasker.InfoText]. -func (t baseConfigTask) InfoText() string { return "" } - -// Run implements [tools.Tasker.Run]. -func (t baseConfigTask) Run(ctx context.Context) error { - // ciutil.PlaceConfigFile("pyproject.toml") - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t baseConfigTask) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t buildTask) InfoText() string { return "Build" } - -// Run implements [tools.Tasker.Run]. -func (t buildTask) Run(ctx context.Context) error { - if _, err := tools.RunCommand(ctx, []string{"uv", "build"}); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t buildTask) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t ruffLintTask) InfoText() string { return "Lint (ruff)" } - -// Run implements [tools.Tasker.Run]. -func (t ruffLintTask) Run(ctx context.Context) error { - if err := pyRun(ctx, ruffLint, "check", "--fix", "./src"); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t ruffLintTask) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t ruffFormatTask) InfoText() string { return "Format (ruff)" } - -// Run implements [tools.Tasker.Run]. -func (t ruffFormatTask) Run(ctx context.Context) error { - if err := pyRun(ctx, ruffFormat, "format", "./src"); err != nil { - return err - } - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t ruffFormatTask) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t pydoclintTask) InfoText() string { return "Lint (pydoclint)" } - -// Run implements [tools.Tasker.Run]. -func (t pydoclintTask) Run(ctx context.Context) error { - if err := pyRun(ctx, pydoclint, "./src"); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t pydoclintTask) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t mypyTask) InfoText() string { return "Type-check (mypy)" } - -// Run implements [tools.Tasker.Run]. -func (t mypyTask) Run(ctx context.Context) error { - if err := pyRun(ctx, mypy, "./src"); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t mypyTask) Post(_ context.Context) error { return nil } - -// pyRun is a wrapper for "uvx" -func pyRun(ctx context.Context, t tools.Tool, trailingArgs ...string) error { - args := slices.Concat( - []string{"uvx", fmt.Sprintf("%s@%s", t.Name, t.Version)}, - trailingArgs, - ) - if _, err := tools.RunCommand(ctx, args); err != nil { - return fmt.Errorf("running 'uvx': %w", err) - } - - return nil -} diff --git a/internal/tools/python/versions.go b/internal/tools/python/versions.go deleted file mode 100644 index 9069758..0000000 --- a/internal/tools/python/versions.go +++ /dev/null @@ -1,26 +0,0 @@ -package pytools - -import ( - "github.com/opensourcecorp/oscar/internal/tools" -) - -var ( - // NOTE: even though ruff is used for both linting & formatting, their implementations differ, - // so we need two distinct [ciutil.Tool]s. - ruffLint = tools.Tool{ - Name: "ruff", - Version: "0.12.7", - } - ruffFormat = tools.Tool{ - Name: "ruff", - Version: "0.12.7", - } - pydoclint = tools.Tool{ - Name: "pydoclint", - Version: "0.6.6", - } - mypy = tools.Tool{ - Name: "mypy", - Version: "1.17.1", - } -) diff --git a/internal/tools/shell/tasks.go b/internal/tools/shell/tasks.go deleted file mode 100644 index 8aab5b0..0000000 --- a/internal/tools/shell/tasks.go +++ /dev/null @@ -1,69 +0,0 @@ -package shtools - -import ( - "context" - "fmt" - - "github.com/opensourcecorp/oscar/internal/tools" -) - -type ( - shellcheckTask struct{} - shfmtTask struct{} -) - -var tasks = []tools.Tasker{ - shellcheckTask{}, - shfmtTask{}, -} - -// Tasks returns the list of CI tasks. -func Tasks(repo tools.Repo) []tools.Tasker { - if repo.HasShell { - return tasks - } - - return nil -} - -// InfoText implements [tools.Tasker.InfoText]. -func (t shellcheckTask) InfoText() string { return "Lint (shellcheck)" } - -// Run implements [tools.Tasker.Run]. -func (t shellcheckTask) Run(ctx context.Context) error { - args := []string{"bash", "-c", fmt.Sprintf(` - shopt -s globstar - ls **/*.sh || exit 0 - %s **/*.sh`, - shellcheck.Name, - )} - if _, err := tools.RunCommand(ctx, args); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t shellcheckTask) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t shfmtTask) InfoText() string { return "Format (shfmt)" } - -// Run implements [tools.Tasker.Run]. -func (t shfmtTask) Run(ctx context.Context) error { - args := []string{"bash", "-c", fmt.Sprintf(` - shopt -s globstar - ls **/*.sh || exit 0 - %s **/*.sh`, - shfmt.Name, - )} - if _, err := tools.RunCommand(ctx, args); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t shfmtTask) Post(_ context.Context) error { return nil } diff --git a/internal/tools/shell/versions.go b/internal/tools/shell/versions.go deleted file mode 100644 index bec2cc4..0000000 --- a/internal/tools/shell/versions.go +++ /dev/null @@ -1,16 +0,0 @@ -package shtools - -import ( - "github.com/opensourcecorp/oscar/internal/tools" -) - -var ( - shellcheck = tools.Tool{ - Name: "shellcheck", - Version: "v0.11.0", - } - shfmt = tools.Tool{ - Name: "shfmt", - Version: "v3.12.0", - } -) diff --git a/internal/tools/toolcfg/.yamlfmt b/internal/tools/toolcfg/.yamlfmt deleted file mode 100644 index 6e7519f..0000000 --- a/internal/tools/toolcfg/.yamlfmt +++ /dev/null @@ -1,5 +0,0 @@ ---- -formatter: - type: "basic" - line_ending: "lf" - include_document_start: true diff --git a/internal/tools/yaml/tasks.go b/internal/tools/yaml/tasks.go deleted file mode 100644 index 6dd3cb2..0000000 --- a/internal/tools/yaml/tasks.go +++ /dev/null @@ -1,92 +0,0 @@ -package yamltools - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/opensourcecorp/oscar/internal/tools" - "github.com/opensourcecorp/oscar/internal/tools/toolcfg" -) - -type ( - yamlfmtTask struct{} - yamllintTask struct{} -) - -var tasks = []tools.Tasker{ - yamlfmtTask{}, - yamllintTask{}, -} - -// Tasks returns the list of CI tasks. -func Tasks(repo tools.Repo) []tools.Tasker { - if repo.HasYaml { - return tasks - } - - return nil -} - -// InfoText implements [tools.Tasker.InfoText]. -func (t yamllintTask) InfoText() string { return "Lint (yamllint)" } - -// Run implements [tools.Tasker.Run]. -func (t yamllintTask) Run(ctx context.Context) error { - cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(yamllint.ConfigFilePath)) - if err != nil { - return fmt.Errorf("reading embedded file contents: %w", err) - } - - if err := os.WriteFile(yamllint.ConfigFilePath, cfgFileContents, 0644); err != nil { - return fmt.Errorf("writing config file: %w", err) - } - - args := []string{"bash", "-c", - fmt.Sprintf( - `%s --strict --config-file %s $(%s)`, - yamllint.Name, yamllint.ConfigFilePath, tools.GetFileTypeListerCommand("yaml"), - ), - } - - if _, err := tools.RunCommand(ctx, args); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t yamllintTask) Post(_ context.Context) error { return nil } - -// InfoText implements [tools.Tasker.InfoText]. -func (t yamlfmtTask) InfoText() string { return "Format (yamlfmt)" } - -// Run implements [tools.Tasker.Run]. -func (t yamlfmtTask) Run(ctx context.Context) error { - cfgFileContents, err := toolcfg.Files.ReadFile(filepath.Base(yamlfmt.ConfigFilePath)) - if err != nil { - return fmt.Errorf("reading embedded file contents: %w", err) - } - - if err := os.WriteFile(yamlfmt.ConfigFilePath, cfgFileContents, 0644); err != nil { - return fmt.Errorf("writing config file: %w", err) - } - - args := []string{"bash", "-c", - fmt.Sprintf( - `%s -conf %s $(%s)`, - yamlfmt.Name, yamlfmt.ConfigFilePath, tools.GetFileTypeListerCommand("yaml"), - ), - } - - if _, err := tools.RunCommand(ctx, args); err != nil { - return err - } - - return nil -} - -// Post implements [tools.Tasker.Post]. -func (t yamlfmtTask) Post(_ context.Context) error { return nil } diff --git a/internal/tools/yaml/versions.go b/internal/tools/yaml/versions.go deleted file mode 100644 index 25513ed..0000000 --- a/internal/tools/yaml/versions.go +++ /dev/null @@ -1,19 +0,0 @@ -package yamltools - -import ( - "os" - "path/filepath" - - "github.com/opensourcecorp/oscar/internal/tools" -) - -var ( - yamlfmt = tools.Tool{ - Name: "yamlfmt", - ConfigFilePath: filepath.Join(os.TempDir(), ".yamlfmt"), - } - yamllint = tools.Tool{ - Name: "yamllint", - ConfigFilePath: filepath.Join(os.TempDir(), ".yamllint"), - } -) diff --git a/mise.toml b/mise.toml index c314308..2cd2cce 100644 --- a/mise.toml +++ b/mise.toml @@ -1,19 +1,28 @@ +[settings] +experimental = true + [tools] github-cli = "prefix:2" go = "prefix:1" +hadolint = "prefix:2" markdownlint-cli2 = "prefix:0.18" # required for markdownlint-cli2 node = "prefix:24" ripgrep = "prefix:14" shellcheck = "prefix:0.11" shfmt = "prefix:3" +terraform = "prefix:1.13" uv = "prefix:0.8" yamlfmt = "prefix:0.17" yamllint = "prefix:1" -# Go tools -"go:honnef.co/go/tools/cmd/staticcheck" = { version = "latest" } -"go:github.com/mgechev/revive" = { version = "prefix:1" } -"go:github.com/kisielk/errcheck" = { version = "prefix:1" } +# Go-specific tools +"go:github.com/kisielk/errcheck" = { version = "prefix:v1" } +"go:github.com/mgechev/revive" = { version = "prefix:v1" } "go:golang.org/x/tools/cmd/goimports" = { version = "latest" } -"go:golang.org/x/vuln/cmd/govulncheck" = { version = "prefix:1" } +"go:golang.org/x/vuln/cmd/govulncheck" = { version = "prefix:v1" } +"go:honnef.co/go/tools/cmd/staticcheck" = { version = "latest" } + +# Python-specific tools. Note that others that may also be CLI tools, may be run internally via +# `uvx` instead. +ruff = "prefix:0.13" diff --git a/scripts/test-bootstrap.sh b/scripts/test-bootstrap.sh index 5eed9b8..d7bd9ab 100644 --- a/scripts/test-bootstrap.sh +++ b/scripts/test-bootstrap.sh @@ -3,7 +3,7 @@ set -euo pipefail shopt -s globstar _setup() { - cp ./internal/tools/toolcfg/pyproject.toml . + cp ./internal/tasks/tools/toolcfg/pyproject.toml . mkdir -p ./src cp -r ./testdata/python/src ./src/test_package rename 's/\.test//g' -- ./src/** From eb1d78924de81a3e7778ea6ac56113e4f9b55227 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 14 Sep 2025 20:20:21 -0500 Subject: [PATCH 09/38] Empty commit to trigger workflow From f9dacf1d735c00cff6ae4a6e44d59d737088cb5c Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 14 Sep 2025 20:30:05 -0500 Subject: [PATCH 10/38] Try bumping the mise version that GHA uses --- .github/workflows/main.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b9b0dce..3bc549b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -25,12 +25,11 @@ jobs: - name: Set up mise uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # release v3.2.0 with: - version: "2025.8.1" + version: "2025.9.10" install: true # runs `mise install` cache: true - name: Run CI run: make ci - # - name: Set up Docker Buildx # uses: docker/setup-buildx-action@v2 From 859f9e34905f0acbc32e49fcb5aeb10303a0562d Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 14 Sep 2025 21:11:53 -0500 Subject: [PATCH 11/38] Consolidate some banner-printing behavior --- internal/consts/consts.go | 2 +- internal/tasks/ci/run.go | 51 +++++++++++---------------- internal/tasks/delivery/run.go | 56 +++++++++++++---------------- internal/tasks/util/types.go | 12 +++---- internal/tasks/util/util.go | 64 ++++++++++++++++++++++++---------- 5 files changed, 96 insertions(+), 89 deletions(-) diff --git a/internal/consts/consts.go b/internal/consts/consts.go index eddad8c..7934546 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -13,7 +13,7 @@ const ( // MiseVersion is the default version of mise to install if not present. Can be overridden via // the `MISE_VERSION` env var, which is checked elsewhere. - MiseVersion = "v2025.8.21" + MiseVersion = "v2025.9.10" // DefaultOscarCfgFileName is the default basename of oscar's config file. DefaultOscarCfgFileName = "oscar.yaml" diff --git a/internal/tasks/ci/run.go b/internal/tasks/ci/run.go index 99b966a..d9feaf9 100644 --- a/internal/tasks/ci/run.go +++ b/internal/tasks/ci/run.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "strings" "time" "github.com/opensourcecorp/oscar/internal/consts" @@ -21,14 +20,9 @@ import ( taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) -// getCITasksMap assembles the overall list of CI tasks, keyed by their language/tooling name -func getCITasksMap(ctx context.Context) (taskutil.TasksMap, error) { - repo, err := taskutil.GetRepoComposition(ctx) - if err != nil { - return nil, fmt.Errorf("getting repo composition: %w", err) - } - - out := make(taskutil.TasksMap) +// getCITaskMap assembles the overall list of CI tasks, keyed by their language/tooling name +func getCITaskMap(repo taskutil.Repo) (taskutil.TaskMap, error) { + out := make(taskutil.TaskMap) for langName, getTasksFunc := range map[string]func(taskutil.Repo) []taskutil.Tasker{ "Versioning": versiontools.NewTasksForCI, "Go": gotools.NewTasksForCI, @@ -46,8 +40,7 @@ func getCITasksMap(ctx context.Context) (taskutil.TasksMap, error) { } if len(out) > 0 { - fmt.Print(repo.String()) - iprint.Debugf("getCITasksMap output: %#v\n", out) + iprint.Debugf("getCITaskMap output: %#v\n", out) } return out, nil @@ -62,34 +55,36 @@ func Run(ctx context.Context) (err error) { } }() - tasksMap, err := getCITasksMap(ctx) + repo, err := taskutil.GetRepoComposition(ctx) + if err != nil { + return fmt.Errorf("getting repo composition: %w", err) + } + + taskMap, err := getCITaskMap(repo) if err != nil { return err } - run, err := taskutil.NewRun(ctx, tasksMap) + run, err := taskutil.NewRun(ctx, "CI") if err != nil { return fmt.Errorf("internal error setting up run info: %w", err) } + run.PrintRunTypeBanner() + + fmt.Print(repo.String()) + // For tracking any changes to Git status etc. after each CI Task runs gitCI, err := git.NewForCI(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } - for lang, tasks := range tasksMap { - langNameBannerPadding := strings.Repeat("=", run.LongestLanguageNameLength-len(lang)/2) - fmt.Printf( - "%s%s %s %s%s\n", - strings.Repeat("=", 24), langNameBannerPadding, lang, langNameBannerPadding, strings.Repeat("=", 24), - ) + for lang, tasks := range taskMap { + run.PrintTaskMapBanner(lang) for _, task := range tasks { taskStartTime := time.Now() - - taskBannerPadding := strings.Repeat(".", run.LongestInfoTextLength-len(task.InfoText())) - // NOTE: no trailing newline on purpose - fmt.Printf("> %s %s............", task.InfoText(), taskBannerPadding) + run.PrintTaskBanner(task) // NOTE: this error is checked later, when we can check the Run, Post, and git-diff // potential errors together @@ -133,16 +128,10 @@ func Run(ctx context.Context) (err error) { } if len(run.Failures) > 0 { - iprint.Errorf("\n================================================================\n") - iprint.Errorf("The following checks failed and/or caused a git diff: (%s)\n", taskutil.RunDurationString(run.StartTime)) - for _, f := range run.Failures { - iprint.Errorf("- %s\n", f) - } - iprint.Errorf("================================================================\n\n") - return errors.New("one or more CI checks failed") + return run.ReportFailure(err) } - fmt.Printf("All checks passed! (%s)\n", taskutil.RunDurationString(run.StartTime)) + run.ReportSuccess() return err } diff --git a/internal/tasks/delivery/run.go b/internal/tasks/delivery/run.go index 94107e7..8fcef85 100644 --- a/internal/tasks/delivery/run.go +++ b/internal/tasks/delivery/run.go @@ -5,24 +5,19 @@ import ( "errors" "fmt" "os" - "strings" "time" "github.com/opensourcecorp/oscar/internal/consts" iprint "github.com/opensourcecorp/oscar/internal/print" + "github.com/opensourcecorp/oscar/internal/tasks/ci" gotools "github.com/opensourcecorp/oscar/internal/tasks/tools/go" taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) -// getDeliveryTasksMap assembles the overall list of Delivery tasks, keyed by their language/tooling +// getDeliveryTaskMap assembles the overall list of Delivery tasks, keyed by their language/tooling // name. -func getDeliveryTasksMap(ctx context.Context) (taskutil.TasksMap, error) { - repo, err := taskutil.GetRepoComposition(ctx) - if err != nil { - return nil, fmt.Errorf("getting repo composition: %w", err) - } - - out := make(taskutil.TasksMap) +func getDeliveryTaskMap(repo taskutil.Repo) (taskutil.TaskMap, error) { + out := make(taskutil.TaskMap) for langName, getTasksFunc := range map[string]func(taskutil.Repo) []taskutil.Tasker{ // "Version": versiontools.TasksForDelivery, "Go": gotools.TasksForDelivery, @@ -38,8 +33,7 @@ func getDeliveryTasksMap(ctx context.Context) (taskutil.TasksMap, error) { } if len(out) > 0 { - fmt.Print(repo.String()) - iprint.Debugf("getDeliveryTasksMap output: %#v\n", out) + iprint.Debugf("getDeliveryTaskMap output: %#v\n", out) } return out, nil @@ -47,6 +41,10 @@ func getDeliveryTasksMap(ctx context.Context) (taskutil.TasksMap, error) { // Run defines the behavior for running all Delivery tasks for the repository. func Run(ctx context.Context) (err error) { + if err := ci.Run(ctx); err != nil { + return fmt.Errorf("running CI tasks before Delivery tasks: %w", err) + } + // The mise config that oscar uses is written during init, so be sure to defer its removal here defer func() { if rmErr := os.RemoveAll(consts.MiseConfigFileName); rmErr != nil { @@ -54,29 +52,31 @@ func Run(ctx context.Context) (err error) { } }() - tasksMap, err := getDeliveryTasksMap(ctx) + repo, err := taskutil.GetRepoComposition(ctx) + if err != nil { + return fmt.Errorf("getting repo composition: %w", err) + } + + taskMap, err := getDeliveryTaskMap(repo) if err != nil { return err } - run, err := taskutil.NewRun(ctx, tasksMap) + run, err := taskutil.NewRun(ctx, "Deliver") if err != nil { return fmt.Errorf("internal error setting up run info: %w", err) } - for lang, tasks := range tasksMap { - langNameBannerPadding := strings.Repeat("=", run.LongestLanguageNameLength-len(lang)/2) - fmt.Printf( - "%s%s %s %s%s\n", - strings.Repeat("=", 24), langNameBannerPadding, lang, langNameBannerPadding, strings.Repeat("=", 24), - ) + run.PrintRunTypeBanner() + + fmt.Print(repo.String()) + + for lang, tasks := range taskMap { + run.PrintTaskMapBanner(lang) for _, task := range tasks { taskStartTime := time.Now() - - taskBannerPadding := strings.Repeat(".", run.LongestInfoTextLength-len(task.InfoText())) - // NOTE: no trailing newline on purpose - fmt.Printf("> %s %s............", task.InfoText(), taskBannerPadding) + run.PrintTaskBanner(task) // NOTE: this error is checked later, when we can check the Run, Post, and git-diff // potential errors together @@ -96,16 +96,10 @@ func Run(ctx context.Context) (err error) { } if len(run.Failures) > 0 { - iprint.Errorf("\n================================================================\n") - iprint.Errorf("The following tasks failed: (%s)\n", taskutil.RunDurationString(run.StartTime)) - for _, f := range run.Failures { - iprint.Errorf("- %s\n", f) - } - iprint.Errorf("================================================================\n\n") - return errors.New("one or more Delivery tasks failed") + return run.ReportFailure(err) } - fmt.Printf("All tasks succeeded! (%s)\n", taskutil.RunDurationString(run.StartTime)) + run.ReportSuccess() return err } diff --git a/internal/tasks/util/types.go b/internal/tasks/util/types.go index 99caa1f..dcb4dd3 100644 --- a/internal/tasks/util/types.go +++ b/internal/tasks/util/types.go @@ -16,8 +16,8 @@ type Tasker interface { Post(ctx context.Context) error } -// TasksMap aliases a map of a Task's language/tooling name to its list of Tasks. -type TasksMap map[string][]Tasker +// TaskMap aliases a map of a Task's language/tooling name to its list of Tasks. +type TaskMap map[string][]Tasker // A Tool defines information about a tool used for running oscar's tasks. A Tool should be defined // if a language etc. cannot perform the task itself. For example, you would not need a Tool to @@ -33,13 +33,11 @@ type Tool struct { // A Run holds metadata about an instance of an oscar subcommand run, e.g. a list of Tasks for the // `ci` subcommand. type Run struct { - // TasksMap TasksMap + // The "type" of Run as an informative string, i.e. "CI", "Deliver", etc. Used in + // banner-printing in [Run.PrintRunTypeBanner]. + Type string // A timestamp for storing when the overall run started. StartTime time.Time - // The length of the longest language/tooling name string. - LongestLanguageNameLength int - // The length of the longest string output by [Tasker.InfoText]. - LongestInfoTextLength int // Keeps track of all task failures. Failures []string } diff --git a/internal/tasks/util/util.go b/internal/tasks/util/util.go index 9941a05..61c1e7a 100644 --- a/internal/tasks/util/util.go +++ b/internal/tasks/util/util.go @@ -83,33 +83,59 @@ func InitSystem(ctx context.Context) error { } // NewRun returns a populated [Run]. -func NewRun(ctx context.Context, tasksMap TasksMap) (Run, error) { +func NewRun(ctx context.Context, runType string) (Run, error) { // Handle system init if err := InitSystem(ctx); err != nil { return Run{}, fmt.Errorf("initializing system: %w", err) } - // Vars for determining text padding in output banners - var ( - longestLanguageNameLength int - longestInfoTextLength int + return Run{ + Type: runType, + StartTime: time.Now(), + Failures: make([]string, 0), + }, nil +} + +// PrintRunTypeBanner prints a banner about the type of [Run] underway. +func (run Run) PrintRunTypeBanner() { + padding := 9 + fmt.Printf("%s\n", strings.Repeat("@", len(run.Type)+padding)) + fmt.Printf("@ Run: %s @\n", run.Type) + fmt.Printf("%s\n\n", strings.Repeat("@", len(run.Type)+padding)) +} + +// PrintTaskMapBanner prints a banner about the [TaskMap] being run. +func (run Run) PrintTaskMapBanner(lang string) { + fmt.Printf( + "=== %s %s>\n", + lang, strings.Repeat("=", 64-len(lang)), ) - for lang, taskMap := range tasksMap { - longestLanguageNameLength = max(longestLanguageNameLength, len(lang)) - for _, task := range taskMap { - longestInfoTextLength = max(longestInfoTextLength, len(task.InfoText())) - } +} + +// PrintTaskBanner prints a banner about the Task being run. +func (run Run) PrintTaskBanner(task Tasker) { + // NOTE: no trailing newline on purpose + fmt.Printf("> %s %s............", task.InfoText(), strings.Repeat(".", 32-len(task.InfoText()))) +} + +// ReportSuccess prints information about the success of a [Run]. +func (run Run) ReportSuccess() { + fmt.Printf("All tasks succeeded! (%s)\n\n", RunDurationString(run.StartTime)) +} + +// ReportFailure prints information about the failure of a [Run]. It takes an `error` arg in case +// the caller is expecting to return a joined error because of e.g. deferred calls or later-checked +// errors that an outer variable already holds. +func (run Run) ReportFailure(err error) error { + iprint.Errorf("\n%s\n", strings.Repeat("=", 65)) + iprint.Errorf("The following tasks failed: (%s)\n", RunDurationString(run.StartTime)) + for _, f := range run.Failures { + iprint.Errorf("- %s\n", f) } - iprint.Debugf("longestLanguageNameLength: %d\n", longestLanguageNameLength) - iprint.Debugf("longestInfoTextLength: %d\n", longestInfoTextLength) + iprint.Errorf("%s\n\n", strings.Repeat("=", 65)) - return Run{ - // TasksMap: tasksMap, - StartTime: time.Now(), - LongestLanguageNameLength: longestLanguageNameLength, - LongestInfoTextLength: longestInfoTextLength, - Failures: make([]string, 0), - }, nil + err = errors.Join(err, errors.New("one or more tasks failed")) + return err } // RunCommand takes a string slice containing a command & its args to run, and returns a consistent From 8c057dcf6cd62afb0ffc31f0772c4fc758a9d8d6 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 14 Sep 2025 21:49:45 -0500 Subject: [PATCH 12/38] More consolidation etc --- internal/git/deliver.go | 1 + internal/oscarcfg/config.go | 8 +- internal/tasks/ci/run.go | 21 ++- internal/tasks/delivery/run.go | 32 ++--- internal/tasks/tools/go/deliver.go | 36 +++-- internal/tasks/util/core_types.go | 52 ++++++++ internal/tasks/util/doc.go | 2 +- internal/tasks/util/repo.go | 110 +++++++++++++++ internal/tasks/util/run.go | 84 ++++++++++++ internal/tasks/util/{util.go => system.go} | 148 ++------------------- internal/tasks/util/types.go | 88 ------------ 11 files changed, 306 insertions(+), 276 deletions(-) create mode 100644 internal/tasks/util/core_types.go create mode 100644 internal/tasks/util/repo.go create mode 100644 internal/tasks/util/run.go rename internal/tasks/util/{util.go => system.go} (60%) delete mode 100644 internal/tasks/util/types.go diff --git a/internal/git/deliver.go b/internal/git/deliver.go index cbaa25d..2c632cc 100644 --- a/internal/git/deliver.go +++ b/internal/git/deliver.go @@ -28,6 +28,7 @@ func NewForDelivery(ctx context.Context) (*Delivery, error) { if err != nil { return nil, err } + iprint.Debugf("latest Git tag: '%s'\n", latestTag) cfg, err := oscarcfg.Get() if err != nil { diff --git a/internal/oscarcfg/config.go b/internal/oscarcfg/config.go index 379882b..c8653a7 100644 --- a/internal/oscarcfg/config.go +++ b/internal/oscarcfg/config.go @@ -45,7 +45,7 @@ type GoGitHubRelease struct { // Get returns a populated [Config] based on the oscar config file location. If `path` is not // provided, it will default to looking in the calling directory. -func Get(pathOverride ...string) (*Config, error) { +func Get(pathOverride ...string) (Config, error) { path := consts.DefaultOscarCfgFileName // Handle the override so we can test this function, and use it in other ways (like checking the @@ -56,14 +56,14 @@ func Get(pathOverride ...string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { - return nil, fmt.Errorf("reading oscar config file: %w", err) + return Config{}, fmt.Errorf("reading oscar config file: %w", err) } iprint.Debugf("data read from oscar config file: %s\n", string(data)) var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("unmarshalling oscar config file '%s': %w", path, err) + return Config{}, fmt.Errorf("unmarshalling oscar config file '%s': %w", path, err) } - return &cfg, nil + return cfg, nil } diff --git a/internal/tasks/ci/run.go b/internal/tasks/ci/run.go index d9feaf9..c8bbf6d 100644 --- a/internal/tasks/ci/run.go +++ b/internal/tasks/ci/run.go @@ -39,9 +39,7 @@ func getCITaskMap(repo taskutil.Repo) (taskutil.TaskMap, error) { } } - if len(out) > 0 { - iprint.Debugf("getCITaskMap output: %#v\n", out) - } + iprint.Debugf("getCITaskMap output: %#v\n", out) return out, nil } @@ -55,25 +53,22 @@ func Run(ctx context.Context) (err error) { } }() - repo, err := taskutil.GetRepoComposition(ctx) + run, err := taskutil.NewRun(ctx, "CI") if err != nil { - return fmt.Errorf("getting repo composition: %w", err) + return fmt.Errorf("internal error setting up run info: %w", err) } - taskMap, err := getCITaskMap(repo) + repo, err := taskutil.NewRepo(ctx) if err != nil { - return err + return fmt.Errorf("getting repo composition: %w", err) } + fmt.Print(repo.String()) - run, err := taskutil.NewRun(ctx, "CI") + taskMap, err := getCITaskMap(repo) if err != nil { - return fmt.Errorf("internal error setting up run info: %w", err) + return err } - run.PrintRunTypeBanner() - - fmt.Print(repo.String()) - // For tracking any changes to Git status etc. after each CI Task runs gitCI, err := git.NewForCI(ctx) if err != nil { diff --git a/internal/tasks/delivery/run.go b/internal/tasks/delivery/run.go index 8fcef85..35a7518 100644 --- a/internal/tasks/delivery/run.go +++ b/internal/tasks/delivery/run.go @@ -19,12 +19,10 @@ import ( func getDeliveryTaskMap(repo taskutil.Repo) (taskutil.TaskMap, error) { out := make(taskutil.TaskMap) for langName, getTasksFunc := range map[string]func(taskutil.Repo) []taskutil.Tasker{ - // "Version": versiontools.TasksForDelivery, - "Go": gotools.TasksForDelivery, - // "Python": pytools.TasksForDelivery, - // "YAML": yamltools.TasksForDelivery, - // "Shell": shtools.TasksForDelivery, - // "Markdown": mdtools.TasksForDelivery, + "Go": gotools.NewTasksForDelivery, + // "Python": pytools.NewTasksForDelivery, + // "Terraform": tftools.NewTasksForDelivery, + // "Markdown": mdtools.NewTasksForDelivery, } { tasks := getTasksFunc(repo) if len(tasks) > 0 { @@ -32,15 +30,14 @@ func getDeliveryTaskMap(repo taskutil.Repo) (taskutil.TaskMap, error) { } } - if len(out) > 0 { - iprint.Debugf("getDeliveryTaskMap output: %#v\n", out) - } + iprint.Debugf("getDeliveryTaskMap output: %#v\n", out) return out, nil } // Run defines the behavior for running all Delivery tasks for the repository. func Run(ctx context.Context) (err error) { + // We intentionally run CI tasks before allowing any Delivery tasks to begin if err := ci.Run(ctx); err != nil { return fmt.Errorf("running CI tasks before Delivery tasks: %w", err) } @@ -52,25 +49,22 @@ func Run(ctx context.Context) (err error) { } }() - repo, err := taskutil.GetRepoComposition(ctx) + run, err := taskutil.NewRun(ctx, "Deliver") if err != nil { - return fmt.Errorf("getting repo composition: %w", err) + return fmt.Errorf("internal error setting up run info: %w", err) } - taskMap, err := getDeliveryTaskMap(repo) + repo, err := taskutil.NewRepo(ctx) if err != nil { - return err + return fmt.Errorf("getting repo composition: %w", err) } + fmt.Print(repo.String()) - run, err := taskutil.NewRun(ctx, "Deliver") + taskMap, err := getDeliveryTaskMap(repo) if err != nil { - return fmt.Errorf("internal error setting up run info: %w", err) + return err } - run.PrintRunTypeBanner() - - fmt.Print(repo.String()) - for lang, tasks := range taskMap { run.PrintTaskMapBanner(lang) diff --git a/internal/tasks/tools/go/deliver.go b/internal/tasks/tools/go/deliver.go index e8da680..18abef3 100644 --- a/internal/tasks/tools/go/deliver.go +++ b/internal/tasks/tools/go/deliver.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/opensourcecorp/oscar/internal/oscarcfg" taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) @@ -14,13 +15,11 @@ type ( ghRelease struct{ taskutil.Tool } ) -// TasksForDelivery returns the list of Delivery tasks. -func TasksForDelivery(repo taskutil.Repo) []taskutil.Tasker { +// NewTasksForDelivery returns the list of Delivery tasks. +func NewTasksForDelivery(repo taskutil.Repo) []taskutil.Tasker { if repo.HasGo { return []taskutil.Tasker{ - ghRelease{ - Tool: taskutil.Tool{}, - }, + ghRelease{}, } } @@ -32,6 +31,26 @@ func (t ghRelease) InfoText() string { return "GitHub Release" } // Exec implements [taskutil.Tasker.Exec]. func (t ghRelease) Exec(ctx context.Context) error { + cfg, err := oscarcfg.Get() + if err != nil { + return err + } + + var buildErr error + for _, src := range cfg.Deliver.GoGitHubRelease.BuildSources { + buildErr = goBuild(ctx, src) + } + if buildErr != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t ghRelease) Post(_ context.Context) error { return nil } + +func goBuild(ctx context.Context, src string) error { targetDir := "build" if err := os.RemoveAll(targetDir); err != nil { @@ -54,9 +73,7 @@ func (t ghRelease) Exec(ctx context.Context) error { goos := splits[0] goarch := splits[1] - binName := "oscar" - - src := "./cmd/oscar" + binName := filepath.Base(src) target := filepath.Join(targetDir, fmt.Sprintf("%s-%s-%s", binName, goos, goarch)) if _, err := taskutil.RunCommand(ctx, []string{"bash", "-c", fmt.Sprintf(` @@ -76,6 +93,3 @@ func (t ghRelease) Exec(ctx context.Context) error { return nil } - -// Post implements [taskutil.Tasker.Post]. -func (t ghRelease) Post(_ context.Context) error { return nil } diff --git a/internal/tasks/util/core_types.go b/internal/tasks/util/core_types.go new file mode 100644 index 0000000..bc6cecb --- /dev/null +++ b/internal/tasks/util/core_types.go @@ -0,0 +1,52 @@ +package taskutil + +import ( + "context" + "strings" + + iprint "github.com/opensourcecorp/oscar/internal/print" +) + +// Tasker defines the method set for working with metadata for a given CI Task. +type Tasker interface { + // InfoText should return a human-readable display string that describes the task, e.g. "Run + // tests". + InfoText() string + // Exec should perform the actual task's actions. + Exec(ctx context.Context) error + // Post should perform any post-run actions for the task, if necessary. + Post(ctx context.Context) error +} + +// TaskMap aliases a map of a Task's language/tooling name to its list of Tasks. +type TaskMap map[string][]Tasker + +// A Tool defines information about a tool used for running oscar's tasks. A Tool should be defined +// if a language etc. cannot perform the task itself. For example, you would not need a Tool to +// represent a task that runs "go test", but you *would* need a tool to represent a task that runs +// the external "staticcheck" linter for Go. +type Tool struct { + // TODO + RunArgs []string + // The path to the tool's config file, if it has one to use. + ConfigFilePath string +} + +// RenderRunCommandArgs uses [Tool.RunArgs] and does naive templating to replace certain values +// before being used. +// +// This is useful because when instantiating [Tasker]s, sometimes the fields need to be +// self-referential within the struct. For example, if a [Tool.RunArgs] needs to specify a config +// file path, but it can't know that value until instantiation (even though it's likely defined +// right below it in [Tool.ConfigFilePath]), you can write the `RunArgs` to have a +// `{{ConfigFilePath}}` placeholder that will be interpolated when calling this function. +func (t Tool) RenderRunCommandArgs() []string { + out := make([]string, len(t.RunArgs)) + for i, arg := range t.RunArgs { + out[i] = strings.ReplaceAll(arg, `{{ConfigFilePath}}`, t.ConfigFilePath) + } + + iprint.Debugf("RenderRunCommandArgs: %#v\n", out) + + return out +} diff --git a/internal/tasks/util/doc.go b/internal/tasks/util/doc.go index 083cc6d..6f05bb9 100644 --- a/internal/tasks/util/doc.go +++ b/internal/tasks/util/doc.go @@ -1,3 +1,3 @@ -// Package taskutil contains type definitions and helper functions for working with Tasks & Tools +// Package taskutil contains type definitions and functionality for working with Tasks & Tools // across the oscar codebase. package taskutil diff --git a/internal/tasks/util/repo.go b/internal/tasks/util/repo.go new file mode 100644 index 0000000..fa0fcd9 --- /dev/null +++ b/internal/tasks/util/repo.go @@ -0,0 +1,110 @@ +package taskutil + +import ( + "context" + "errors" + + iprint "github.com/opensourcecorp/oscar/internal/print" +) + +// A Repo stores information about the contents of the repository being run against. +type Repo struct { + HasGo bool + HasPython bool + HasShell bool + HasTerraform bool + HasContainerfile bool + HasYaml bool + HasMarkdown bool +} + +// String implements the [fmt.Stringer] interface. +func (repo Repo) String() string { + var out string + + out += "The following file types were found in this repo, and tasks will be run against them:\n" + + if repo.HasGo { + out += "- Go\n" + } + if repo.HasPython { + out += "- Python\n" + } + if repo.HasShell { + out += "- Shell (sh, bash, etc.)\n" + } + if repo.HasTerraform { + out += "- Terraform\n" + } + if repo.HasContainerfile { + out += "- Containerfile\n" + } + if repo.HasYaml { + out += "- YAML\n" + } + if repo.HasMarkdown { + out += "- Markdown\n" + } + + // One more newline for padding + out += "\n" + + return out +} + +// NewRepo returns a populated [Repo]. +func NewRepo(ctx context.Context) (Repo, error) { + var errs error + + hasGo, err := filesExistInTree(ctx, GetFileTypeListerCommand("go")) + if err != nil { + errs = errors.Join(errs, err) + } + + hasPython, err := filesExistInTree(ctx, GetFileTypeListerCommand("py")) + if err != nil { + errs = errors.Join(errs, err) + } + + hasShell, err := filesExistInTree(ctx, GetFileTypeListerCommand("sh")) + if err != nil { + errs = errors.Join(errs, err) + } + + hasTerraform, err := filesExistInTree(ctx, GetFileTypeListerCommand("tf")) + if err != nil { + errs = errors.Join(errs, err) + } + + hasContainerfile, err := filesExistInTree(ctx, GetFileTypeListerCommand("containerfile")) + if err != nil { + errs = errors.Join(errs, err) + } + + hasYaml, err := filesExistInTree(ctx, GetFileTypeListerCommand("yaml")) + if err != nil { + errs = errors.Join(errs, err) + } + + hasMarkdown, err := filesExistInTree(ctx, GetFileTypeListerCommand("md")) + if err != nil { + errs = errors.Join(errs, err) + } + + if errs != nil { + return Repo{}, errs + } + + repo := Repo{ + HasGo: hasGo, + HasPython: hasPython, + HasShell: hasShell, + HasTerraform: hasTerraform, + HasContainerfile: hasContainerfile, + HasYaml: hasYaml, + HasMarkdown: hasMarkdown, + } + iprint.Debugf("repo composition: %+v\n", repo) + + return repo, nil +} diff --git a/internal/tasks/util/run.go b/internal/tasks/util/run.go new file mode 100644 index 0000000..8ba05f7 --- /dev/null +++ b/internal/tasks/util/run.go @@ -0,0 +1,84 @@ +package taskutil + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + iprint "github.com/opensourcecorp/oscar/internal/print" +) + +// A Run holds metadata about an instance of an oscar subcommand run, e.g. a list of Tasks for the +// `ci` subcommand. +type Run struct { + // The "type" of Run as an informative string, i.e. "CI", "Deliver", etc. Used in + // banner-printing in [Run.PrintRunTypeBanner]. + Type string + // A timestamp for storing when the overall run started. + StartTime time.Time + // Keeps track of all task failures. + Failures []string +} + +// NewRun returns a populated [Run]. +func NewRun(ctx context.Context, runType string) (Run, error) { + // Kind of wonky, but print the banner first + Run{Type: runType}.PrintRunTypeBanner() + + // Handle system init + if err := InitSystem(ctx); err != nil { + return Run{}, fmt.Errorf("initializing system: %w", err) + } + + return Run{ + Type: runType, + StartTime: time.Now(), + Failures: make([]string, 0), + }, nil +} + +// PrintRunTypeBanner prints a banner about the type of [Run] underway. +func (run Run) PrintRunTypeBanner() { + // this padding accounts for leading text length before the run.Type string + padding := 9 + bannerChar := "#" + fmt.Printf("%s\n", strings.Repeat(bannerChar, len(run.Type)+padding)) + fmt.Printf("%s Run: %s #\n", bannerChar, run.Type) + fmt.Printf("%s\n\n", strings.Repeat(bannerChar, len(run.Type)+padding)) +} + +// PrintTaskMapBanner prints a banner about the [TaskMap] being run. +func (run Run) PrintTaskMapBanner(lang string) { + fmt.Printf( + "=== %s %s>\n", + lang, strings.Repeat("=", 64-len(lang)), + ) +} + +// PrintTaskBanner prints a banner about the Task being run. +func (run Run) PrintTaskBanner(task Tasker) { + // NOTE: no trailing newline on purpose + fmt.Printf("> %s %s............", task.InfoText(), strings.Repeat(".", 32-len(task.InfoText()))) +} + +// ReportSuccess prints information about the success of a [Run]. +func (run Run) ReportSuccess() { + fmt.Printf("\nAll tasks succeeded! (%s)\n\n", RunDurationString(run.StartTime)) +} + +// ReportFailure prints information about the failure of a [Run]. It takes an `error` arg in case +// the caller is expecting to return a joined error because of e.g. deferred calls or later-checked +// errors that an outer variable already holds. +func (run Run) ReportFailure(err error) error { + iprint.Errorf("\n%s\n", strings.Repeat("=", 65)) + iprint.Errorf("The following tasks failed: (%s)\n", RunDurationString(run.StartTime)) + for _, f := range run.Failures { + iprint.Errorf("- %s\n", f) + } + iprint.Errorf("%s\n\n", strings.Repeat("=", 65)) + + err = errors.Join(err, errors.New("one or more tasks failed")) + return err +} diff --git a/internal/tasks/util/util.go b/internal/tasks/util/system.go similarity index 60% rename from internal/tasks/util/util.go rename to internal/tasks/util/system.go index 61c1e7a..a20cba6 100644 --- a/internal/tasks/util/util.go +++ b/internal/tasks/util/system.go @@ -82,62 +82,6 @@ func InitSystem(ctx context.Context) error { return nil } -// NewRun returns a populated [Run]. -func NewRun(ctx context.Context, runType string) (Run, error) { - // Handle system init - if err := InitSystem(ctx); err != nil { - return Run{}, fmt.Errorf("initializing system: %w", err) - } - - return Run{ - Type: runType, - StartTime: time.Now(), - Failures: make([]string, 0), - }, nil -} - -// PrintRunTypeBanner prints a banner about the type of [Run] underway. -func (run Run) PrintRunTypeBanner() { - padding := 9 - fmt.Printf("%s\n", strings.Repeat("@", len(run.Type)+padding)) - fmt.Printf("@ Run: %s @\n", run.Type) - fmt.Printf("%s\n\n", strings.Repeat("@", len(run.Type)+padding)) -} - -// PrintTaskMapBanner prints a banner about the [TaskMap] being run. -func (run Run) PrintTaskMapBanner(lang string) { - fmt.Printf( - "=== %s %s>\n", - lang, strings.Repeat("=", 64-len(lang)), - ) -} - -// PrintTaskBanner prints a banner about the Task being run. -func (run Run) PrintTaskBanner(task Tasker) { - // NOTE: no trailing newline on purpose - fmt.Printf("> %s %s............", task.InfoText(), strings.Repeat(".", 32-len(task.InfoText()))) -} - -// ReportSuccess prints information about the success of a [Run]. -func (run Run) ReportSuccess() { - fmt.Printf("All tasks succeeded! (%s)\n\n", RunDurationString(run.StartTime)) -} - -// ReportFailure prints information about the failure of a [Run]. It takes an `error` arg in case -// the caller is expecting to return a joined error because of e.g. deferred calls or later-checked -// errors that an outer variable already holds. -func (run Run) ReportFailure(err error) error { - iprint.Errorf("\n%s\n", strings.Repeat("=", 65)) - iprint.Errorf("The following tasks failed: (%s)\n", RunDurationString(run.StartTime)) - for _, f := range run.Failures { - iprint.Errorf("- %s\n", f) - } - iprint.Errorf("%s\n\n", strings.Repeat("=", 65)) - - err = errors.Join(err, errors.New("one or more tasks failed")) - return err -} - // RunCommand takes a string slice containing a command & its args to run, and returns a consistent // error message in case of failure. It also returns the command output, in case the caller needs to // parse it on their own. @@ -166,67 +110,16 @@ func RunCommand(ctx context.Context, cmdArgs []string) (string, error) { return strings.TrimSuffix(string(output), "\n"), nil } -// GetRepoComposition returns a populated [Repo]. -func GetRepoComposition(ctx context.Context) (Repo, error) { - var errs error - - hasGo, err := filesExistInTree(ctx, GetFileTypeListerCommand("go")) - if err != nil { - errs = errors.Join(errs, err) - } - - hasPython, err := filesExistInTree(ctx, GetFileTypeListerCommand("py")) - if err != nil { - errs = errors.Join(errs, err) - } - - hasShell, err := filesExistInTree(ctx, GetFileTypeListerCommand("sh")) - if err != nil { - errs = errors.Join(errs, err) - } - - hasTerraform, err := filesExistInTree(ctx, GetFileTypeListerCommand("tf")) - if err != nil { - errs = errors.Join(errs, err) - } - - hasContainerfile, err := filesExistInTree(ctx, GetFileTypeListerCommand("containerfile")) - if err != nil { - errs = errors.Join(errs, err) - } - - hasYaml, err := filesExistInTree(ctx, GetFileTypeListerCommand("yaml")) - if err != nil { - errs = errors.Join(errs, err) - } - - hasMarkdown, err := filesExistInTree(ctx, GetFileTypeListerCommand("md")) - if err != nil { - errs = errors.Join(errs, err) - } - - if errs != nil { - return Repo{}, errs - } - - repo := Repo{ - HasGo: hasGo, - HasPython: hasPython, - HasShell: hasShell, - HasTerraform: hasTerraform, - HasContainerfile: hasContainerfile, - HasYaml: hasYaml, - HasMarkdown: hasMarkdown, - } - iprint.Debugf("repo composition: %+v\n", repo) - - return repo, nil +// RunDurationString returns a calculated duration used to indicate how long a particular Task (or +// set of Tasks) took to run. +func RunDurationString(t time.Time) string { + return fmt.Sprintf("t: %s", time.Since(t).Round(time.Second/1000).String()) } -// GetFileTypeListerCommand takes a [ripgrep]-known file type, and returns a slice containing a -// command & its args to find matching files. ripgrep is used as the default file-finder across the -// codebase because not only is it fast, but it also supports files like `.gitignore` without any -// extra configuration. +// GetFileTypeListerCommand takes a [ripgrep]-known file type, and returns a string (to be used as +// an arg after `bash -c`) representing a command & its args to find matching files. ripgrep is used +// as the default file-finder across the codebase because not only is it fast, but it also supports +// files like `.gitignore` without any extra configuration. // // [ripgrep]: https://github.com/BurntSushi/ripgrep func GetFileTypeListerCommand(fileType string) string { @@ -241,31 +134,6 @@ func GetFileTypeListerCommand(fileType string) string { } } -// RunDurationString returns a calculated duration used to indicate how long a particular Task (or -// set of Tasks) took to run. -func RunDurationString(t time.Time) string { - return fmt.Sprintf("t: %s", time.Since(t).Round(time.Second/1000).String()) -} - -// RenderRunCommandArgs uses [Tool.RunArgs] and does naive templating to replace certain values -// before being used. -// -// This is useful because when instantiating [Tasker]s, sometimes the fields need to be -// self-referential within the struct. For example, if a [Tool.RunArgs] needs to specify a config -// file path, but it can't know that value until instantiation (even though it's likely defined -// right below it in [Tool.ConfigFilePath]), you can write the `RunArgs` to have a -// `{{ConfigFilePath}}` placeholder that will be interpolated when calling this function. -func (t Tool) RenderRunCommandArgs() []string { - out := make([]string, len(t.RunArgs)) - for i, arg := range t.RunArgs { - out[i] = strings.ReplaceAll(arg, `{{ConfigFilePath}}`, t.ConfigFilePath) - } - - iprint.Debugf("RenderRunCommandArgs: %#v\n", out) - - return out -} - // installMise installs [mise] into [consts.OscarHomeBin], if not found there. // // [mise]: https://mise.jdx.dev diff --git a/internal/tasks/util/types.go b/internal/tasks/util/types.go deleted file mode 100644 index dcb4dd3..0000000 --- a/internal/tasks/util/types.go +++ /dev/null @@ -1,88 +0,0 @@ -package taskutil - -import ( - "context" - "time" -) - -// Tasker defines the method set for working with metadata for a given CI Task. -type Tasker interface { - // InfoText should return a human-readable display string that describes the task, e.g. "Run - // tests". - InfoText() string - // Exec should perform the actual task's actions. - Exec(ctx context.Context) error - // Post should perform any post-run actions for the task, if necessary. - Post(ctx context.Context) error -} - -// TaskMap aliases a map of a Task's language/tooling name to its list of Tasks. -type TaskMap map[string][]Tasker - -// A Tool defines information about a tool used for running oscar's tasks. A Tool should be defined -// if a language etc. cannot perform the task itself. For example, you would not need a Tool to -// represent a task that runs "go test", but you *would* need a tool to represent a task that runs -// the external "staticcheck" linter for Go. -type Tool struct { - // TODO - RunArgs []string - // The path to the tool's config file, if it has one to use. - ConfigFilePath string -} - -// A Run holds metadata about an instance of an oscar subcommand run, e.g. a list of Tasks for the -// `ci` subcommand. -type Run struct { - // The "type" of Run as an informative string, i.e. "CI", "Deliver", etc. Used in - // banner-printing in [Run.PrintRunTypeBanner]. - Type string - // A timestamp for storing when the overall run started. - StartTime time.Time - // Keeps track of all task failures. - Failures []string -} - -// A Repo stores information about the contents of the repository being run against. -type Repo struct { - HasGo bool - HasPython bool - HasShell bool - HasTerraform bool - HasContainerfile bool - HasYaml bool - HasMarkdown bool -} - -// String implements the [fmt.Stringer] interface. -func (repo Repo) String() string { - var out string - - out += "The following file types were found in this repo, and tasks will be run against them:\n" - - if repo.HasGo { - out += "- Go\n" - } - if repo.HasPython { - out += "- Python\n" - } - if repo.HasShell { - out += "- Shell (sh, bash, etc.)\n" - } - if repo.HasTerraform { - out += "- Terraform\n" - } - if repo.HasContainerfile { - out += "- Containerfile\n" - } - if repo.HasYaml { - out += "- YAML\n" - } - if repo.HasMarkdown { - out += "- Markdown\n" - } - - // One more newline for padding - out += "\n" - - return out -} From d0200829597ac0b17b582a3d139c394f6df733c1 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 14 Sep 2025 22:13:39 -0500 Subject: [PATCH 13/38] More tidying up --- internal/oscarcfg/config.go | 15 +++------------ internal/tasks/ci/run.go | 4 +++- internal/tasks/delivery/run.go | 12 +++++++++--- internal/tasks/tools/go/deliver.go | 25 +++++++++++++++++++------ internal/tasks/util/core_types.go | 20 +++++++++++++++++--- 5 files changed, 51 insertions(+), 25 deletions(-) diff --git a/internal/oscarcfg/config.go b/internal/oscarcfg/config.go index c8653a7..fb9c717 100644 --- a/internal/oscarcfg/config.go +++ b/internal/oscarcfg/config.go @@ -14,23 +14,14 @@ type Config struct { // Version is the version string for the codebase. Version string `yaml:"version" json:"version"` // Deliver is the collection of possible deliverable artifacts. - Deliver Deliverables `yaml:"deliver" json:"deliver"` + Deliver *Deliverables `yaml:"deliver" json:"deliver"` // Deploy Deployables `yaml:"deploy" json:"deploy"` } // Deliverables contains a field for each possible deliverable. type Deliverables struct { - // GoBinaries lists out the Go binaries the user wants to build. - GoBinaries []GoBinary `yaml:"go_binaries" json:"go_binaries"` - GoGitHubRelease GoGitHubRelease `yaml:"go_github_release" json:"go_github_release"` -} - -// GoBinary defines the arguments necessary to build Go binaries. While most other Go-related tasks -// should handle the builds as well, this deliverable type is here to allow users to handle the -// resulting artifacts on their own. -type GoBinary struct { - // BuildSource is the filepath to the "main" package to be built. - BuildSource string `yaml:"build_source" json:"build_source"` + // Defines the GitHub Releases the user wants to create + GoGitHubRelease *GoGitHubRelease `yaml:"go_github_release" json:"go_github_release"` } // GoGitHubRelease defines the arguments necessary to create GitHub Releases for Go binaries. diff --git a/internal/tasks/ci/run.go b/internal/tasks/ci/run.go index c8bbf6d..3707f7a 100644 --- a/internal/tasks/ci/run.go +++ b/internal/tasks/ci/run.go @@ -75,7 +75,9 @@ func Run(ctx context.Context) (err error) { return fmt.Errorf("internal error: %w", err) } - for lang, tasks := range taskMap { + for _, lang := range taskMap.SortedKeys() { + tasks := taskMap[lang] + run.PrintTaskMapBanner(lang) for _, task := range tasks { taskStartTime := time.Now() diff --git a/internal/tasks/delivery/run.go b/internal/tasks/delivery/run.go index 35a7518..160a9c8 100644 --- a/internal/tasks/delivery/run.go +++ b/internal/tasks/delivery/run.go @@ -18,13 +18,17 @@ import ( // name. func getDeliveryTaskMap(repo taskutil.Repo) (taskutil.TaskMap, error) { out := make(taskutil.TaskMap) - for langName, getTasksFunc := range map[string]func(taskutil.Repo) []taskutil.Tasker{ + for langName, getTasksFunc := range map[string]func(taskutil.Repo) ([]taskutil.Tasker, error){ "Go": gotools.NewTasksForDelivery, // "Python": pytools.NewTasksForDelivery, // "Terraform": tftools.NewTasksForDelivery, // "Markdown": mdtools.NewTasksForDelivery, } { - tasks := getTasksFunc(repo) + tasks, err := getTasksFunc(repo) + if err != nil { + return nil, err + } + if len(tasks) > 0 { out[langName] = tasks } @@ -65,7 +69,9 @@ func Run(ctx context.Context) (err error) { return err } - for lang, tasks := range taskMap { + for _, lang := range taskMap.SortedKeys() { + tasks := taskMap[lang] + run.PrintTaskMapBanner(lang) for _, task := range tasks { diff --git a/internal/tasks/tools/go/deliver.go b/internal/tasks/tools/go/deliver.go index 18abef3..2126f6a 100644 --- a/internal/tasks/tools/go/deliver.go +++ b/internal/tasks/tools/go/deliver.go @@ -16,18 +16,27 @@ type ( ) // NewTasksForDelivery returns the list of Delivery tasks. -func NewTasksForDelivery(repo taskutil.Repo) []taskutil.Tasker { - if repo.HasGo { - return []taskutil.Tasker{ - ghRelease{}, +func NewTasksForDelivery(repo taskutil.Repo) ([]taskutil.Tasker, error) { + cfg, err := oscarcfg.Get() + if err != nil { + return nil, err + } + + if repo.HasGo && cfg.Deliver != nil { + out := make([]taskutil.Tasker, 0) + + if cfg.Deliver.GoGitHubRelease != nil { + out = append(out, ghRelease{}) } + + return out, nil } - return nil + return nil, nil } // InfoText implements [taskutil.Tasker.InfoText]. -func (t ghRelease) InfoText() string { return "GitHub Release" } +func (t ghRelease) InfoText() string { return "GitHub Releases" } // Exec implements [taskutil.Tasker.Exec]. func (t ghRelease) Exec(ctx context.Context) error { @@ -51,6 +60,10 @@ func (t ghRelease) Exec(ctx context.Context) error { func (t ghRelease) Post(_ context.Context) error { return nil } func goBuild(ctx context.Context, src string) error { + if strings.HasSuffix(src, ".go") { + return fmt.Errorf("provided Go build source '%s' was a file, but must be a path to a package", src) + } + targetDir := "build" if err := os.RemoveAll(targetDir); err != nil { diff --git a/internal/tasks/util/core_types.go b/internal/tasks/util/core_types.go index bc6cecb..00f8126 100644 --- a/internal/tasks/util/core_types.go +++ b/internal/tasks/util/core_types.go @@ -2,6 +2,7 @@ package taskutil import ( "context" + "slices" "strings" iprint "github.com/opensourcecorp/oscar/internal/print" @@ -18,9 +19,6 @@ type Tasker interface { Post(ctx context.Context) error } -// TaskMap aliases a map of a Task's language/tooling name to its list of Tasks. -type TaskMap map[string][]Tasker - // A Tool defines information about a tool used for running oscar's tasks. A Tool should be defined // if a language etc. cannot perform the task itself. For example, you would not need a Tool to // represent a task that runs "go test", but you *would* need a tool to represent a task that runs @@ -50,3 +48,19 @@ func (t Tool) RenderRunCommandArgs() []string { return out } + +// TaskMap aliases a map of a Task's language/tooling name to its list of Tasks. +type TaskMap map[string][]Tasker + +// SortedKeys sorts the keys of the [TaskMap]. Useful for iterating through Tasks in a predictable +// order during runs. +func (tm TaskMap) SortedKeys() []string { + keys := make([]string, 0) + for key := range tm { + keys = append(keys, key) + } + + slices.Sort(keys) + + return keys +} From bc15718ed449a0c3bf75c52d1ed728843d744e45 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 14 Sep 2025 22:44:10 -0500 Subject: [PATCH 14/38] Fix to allow ripgrep to look at hidden files and folders. Also rename a package. --- .github/workflows/main.yaml | 53 +++++++++++++------ docker-compose.yaml | 3 +- internal/tasks/ci/run.go | 4 +- internal/tasks/tools/containerfile/doc.go | 2 - .../tools/{containerfile => containers}/ci.go | 2 +- internal/tasks/tools/containers/doc.go | 3 ++ internal/tasks/tools/toolcfg/.yamlfmt | 9 ++-- internal/tasks/tools/toolcfg/.yamllint | 12 +++-- internal/tasks/util/system.go | 4 +- oscar.yaml | 3 ++ 10 files changed, 65 insertions(+), 30 deletions(-) delete mode 100644 internal/tasks/tools/containerfile/doc.go rename internal/tasks/tools/{containerfile => containers}/ci.go (97%) create mode 100644 internal/tasks/tools/containers/doc.go diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3bc549b..4e2cf52 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,35 +1,55 @@ -name: main +--- +name: "main" on: push: branches: - - main + - "main" pull_request: types: - - opened - - reopened - - synchronize + - "opened" + - "reopened" + - "synchronize" jobs: ci: - runs-on: ubuntu-latest + runs-on: "ubuntu-latest" permissions: - contents: read - packages: write + contents: "read" steps: - - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # release v5.0.0 - - name: Install system packages + - name: "Checkout repository" + uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # release v5.0.0 + - name: "Install system packages" run: | sudo apt-get update sudo apt-get install -y \ make - - name: Set up mise - uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # release v3.2.0 + - name: "Set up mise" + uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # release v3.2.0 with: version: "2025.9.10" install: true # runs `mise install` cache: true - - name: Run CI - run: make ci + - name: "Run CI Tasks" + run: "make ci" + # deliver: + # permissions: + # contents: "read" + # packages: "write" + # steps: + # - name: "Checkout repository" + # uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # release v5.0.0 + # - name: "Install system packages" + # run: | + # sudo apt-get update + # sudo apt-get install -y \ + # make + # - name: "Set up mise" + # uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # release v3.2.0 + # with: + # version: "2025.9.10" + # install: true # runs `mise install` + # cache: true + # - name: "Run Delivery Tasks" + # run: "make deliver" # - name: Set up Docker Buildx # uses: docker/setup-buildx-action@v2 @@ -40,7 +60,8 @@ jobs: # username: ${{ github.repository_owner }} # password: ${{ secrets.GITHUB_TOKEN }} -# # Generate any tags we want for the images (https://github.com/docker/build-push-action/blob/master/docs/advanced/tags-labels.md) +# # Generate any tags we want for the images +# # (https://github.com/docker/build-push-action/blob/master/docs/advanced/tags-labels.md) # - name: Extract metadata (tags, labels) for OCI image # id: meta # uses: docker/metadata-action@v4 diff --git a/docker-compose.yaml b/docker-compose.yaml index d7f4a9b..5150221 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -12,7 +12,8 @@ services: https_proxy: "${https_proxy}" image: "ghrc.io/opensourcecorp/oscar:latest" pull_policy: "if_not_present" - command: ["ci"] + command: + - "ci" environment: http_proxy: "${http_proxy}" https_proxy: "${https_proxy}" diff --git a/internal/tasks/ci/run.go b/internal/tasks/ci/run.go index 3707f7a..c9cbd24 100644 --- a/internal/tasks/ci/run.go +++ b/internal/tasks/ci/run.go @@ -10,7 +10,7 @@ import ( "github.com/opensourcecorp/oscar/internal/consts" "github.com/opensourcecorp/oscar/internal/git" iprint "github.com/opensourcecorp/oscar/internal/print" - containerfiletools "github.com/opensourcecorp/oscar/internal/tasks/tools/containerfile" + containertools "github.com/opensourcecorp/oscar/internal/tasks/tools/containers" gotools "github.com/opensourcecorp/oscar/internal/tasks/tools/go" mdtools "github.com/opensourcecorp/oscar/internal/tasks/tools/markdown" pytools "github.com/opensourcecorp/oscar/internal/tasks/tools/python" @@ -29,7 +29,7 @@ func getCITaskMap(repo taskutil.Repo) (taskutil.TaskMap, error) { "Python": pytools.NewTasksForCI, // "Terraform": tftools.NewTasksForCI, "YAML": yamltools.NewTasksForCI, - "Containerfile": containerfiletools.NewTasksForCI, + "Containerfile": containertools.NewTasksForCI, "Shell": shtools.NewTasksForCI, "Markdown": mdtools.NewTasksForCI, } { diff --git a/internal/tasks/tools/containerfile/doc.go b/internal/tasks/tools/containerfile/doc.go deleted file mode 100644 index 2820883..0000000 --- a/internal/tasks/tools/containerfile/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package containerfiletools contains logic for running tasks for Containerfiles. -package containerfiletools diff --git a/internal/tasks/tools/containerfile/ci.go b/internal/tasks/tools/containers/ci.go similarity index 97% rename from internal/tasks/tools/containerfile/ci.go rename to internal/tasks/tools/containers/ci.go index a1ff5a1..a0bd769 100644 --- a/internal/tasks/tools/containerfile/ci.go +++ b/internal/tasks/tools/containers/ci.go @@ -1,4 +1,4 @@ -package containerfiletools +package containertools import ( "context" diff --git a/internal/tasks/tools/containers/doc.go b/internal/tasks/tools/containers/doc.go new file mode 100644 index 0000000..95cd90c --- /dev/null +++ b/internal/tasks/tools/containers/doc.go @@ -0,0 +1,3 @@ +// Package containertools contains logic for running tasks for OCI containers (including +// Containerfiles). +package containertools diff --git a/internal/tasks/tools/toolcfg/.yamlfmt b/internal/tasks/tools/toolcfg/.yamlfmt index 481d5b2..6f70c49 100644 --- a/internal/tasks/tools/toolcfg/.yamlfmt +++ b/internal/tasks/tools/toolcfg/.yamlfmt @@ -1,8 +1,11 @@ --- - # Rules found at: - formatter: type: "basic" - line_ending: "lf" + eof_newline: true + force_array_style: "block" include_document_start: true + indent: 2 + line_ending: "lf" + max_line_length: 100 + trim_trailing_whitespace: true diff --git a/internal/tasks/tools/toolcfg/.yamllint b/internal/tasks/tools/toolcfg/.yamllint index c344ea6..14d62ac 100644 --- a/internal/tasks/tools/toolcfg/.yamllint +++ b/internal/tasks/tools/toolcfg/.yamllint @@ -2,15 +2,21 @@ extends: "default" # Rules found at: https://yamllint.readthedocs.io/en/stable/rules.html rules: - line-length: - max: 100 - level: "error" + comments: + require-starting-space: true + ignore-shebangs: true + min-spaces-from-content: 1 empty-lines: max: 1 max-start: 0 max-end: 0 level: "error" + line-length: + max: 100 + level: "error" quoted-strings: required: true quote-type: "double" level: "error" + truthy: + check-keys: false # mostly to allow GitHub Actions' "on" key to pass diff --git a/internal/tasks/util/system.go b/internal/tasks/util/system.go index a20cba6..0f729c9 100644 --- a/internal/tasks/util/system.go +++ b/internal/tasks/util/system.go @@ -128,9 +128,9 @@ func GetFileTypeListerCommand(fileType string) string { // *not* find e.g. "Containerfile" switch fileType { case "containerfile": - return `rg --files --glob-case-insensitive --glob='*{Containerfile,Dockerfile}*' || true` + return `rg --hidden --files --glob-case-insensitive --glob='*{Containerfile,Dockerfile}*' || true` default: - return fmt.Sprintf(`rg --files --type '%s' || true`, fileType) + return fmt.Sprintf(`rg --hidden --files --type '%s' || true`, fileType) } } diff --git a/oscar.yaml b/oscar.yaml index 7a85297..f850a14 100644 --- a/oscar.yaml +++ b/oscar.yaml @@ -6,3 +6,6 @@ deliver: build_sources: - "./cmd/oscar" draft: false + container_image: + host: "ghcr.io" + repo: "opensourcecorp/oscar" From 65f983971daf2be4a4c85243d032ebb38b56e7a1 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Mon, 15 Sep 2025 01:02:31 -0500 Subject: [PATCH 15/38] holy crap i got image build and push working --- .github/workflows/main.yaml | 48 ++++---- Containerfile | 3 + Makefile | 13 +- README.md | 3 +- docker-compose.yaml | 8 +- internal/oscarcfg/config.go | 21 +++- internal/tasks/delivery/run.go | 4 +- internal/tasks/tools/containers/deliver.go | 137 +++++++++++++++++++++ internal/tasks/tools/go/deliver.go | 4 +- mise.toml | 1 + oscar.yaml | 5 +- 11 files changed, 206 insertions(+), 41 deletions(-) create mode 100644 internal/tasks/tools/containers/deliver.go diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4e2cf52..6f95767 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -9,6 +9,8 @@ on: - "opened" - "reopened" - "synchronize" +env: + GITHUB_TOKEN: "{{ secrets.GITHUB_TOKEN }}" jobs: ci: runs-on: "ubuntu-latest" @@ -30,29 +32,29 @@ jobs: cache: true - name: "Run CI Tasks" run: "make ci" - # deliver: - # permissions: - # contents: "read" - # packages: "write" - # steps: - # - name: "Checkout repository" - # uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # release v5.0.0 - # - name: "Install system packages" - # run: | - # sudo apt-get update - # sudo apt-get install -y \ - # make - # - name: "Set up mise" - # uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # release v3.2.0 - # with: - # version: "2025.9.10" - # install: true # runs `mise install` - # cache: true - # - name: "Run Delivery Tasks" - # run: "make deliver" -# - name: Set up Docker Buildx -# uses: docker/setup-buildx-action@v2 - + deliver: + permissions: + contents: "read" + packages: "write" + steps: + - name: "Checkout repository" + uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # release v5.0.0 + - name: "Install system packages" + run: | + sudo apt-get update + sudo apt-get install -y \ + make + - name: "Set up mise" + uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # release v3.2.0 + with: + version: "2025.9.10" + install: true # runs `mise install` + cache: true + - name: "Run Delivery Tasks" + run: "make deliver" + # + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v2 # - name: Log in to GHCR # uses: docker/login-action@v2 # with: diff --git a/Containerfile b/Containerfile index 1aae2fd..4a62c85 100644 --- a/Containerfile +++ b/Containerfile @@ -83,4 +83,7 @@ VOLUME /home/oscar/app # oscar's home directory, for caching on the host VOLUME /home/oscar/.oscar +# So e.g. GitHub can tie the image to its source repo +LABEL org.opencontainers.image.source https://github.com/opensourcecorp/oscar + ENTRYPOINT ["/oscar"] diff --git a/Makefile b/Makefile index a08fa7d..071c1ab 100644 --- a/Makefile +++ b/Makefile @@ -11,15 +11,18 @@ BINNAME := oscar BINPATH := ./cmd/$(BINNAME) DOCKER ?= docker -OCI_REGISTRY ?= ghcr.io -OCI_REGISTRY_OWNER ?= opensourcecorp +export IMAGE_REGISTRY ?= ghcr.io +export IMAGE_REGISTRY_OWNER ?= opensourcecorp +export IMAGE_NAME ?= $(BINNAME) +export IMAGE_TAG ?= 'latest' +export IMAGE_URI ?= $(IMAGE_REGISTRY)/$(IMAGE_REGISTRY_OWNER)/$(IMAGE_NAME):$(IMAGE_TAG) SHELL = /usr/bin/env bash -euo pipefail -.PHONY: % - all: ci +FORCE: + ci: clean @$(RUN) go run ./cmd/$(BINNAME)/main.go ci @@ -28,7 +31,7 @@ test: ci # NOTE: oscar builds itself IRL, but having a target here makes it easier to have the Containerfile # have a stage-copiable output -build: +build: FORCE @$(RUN) go build -o ./build/oscar ./cmd/oscar ci-container: diff --git a/README.md b/README.md index 05eedf4..1fa7c62 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,8 @@ Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details about developing `os * CI additions * Terraform * protobuf - * R + * R? * Rust? * CD additions + * GitHub Releases * Publish to ghcr diff --git a/docker-compose.yaml b/docker-compose.yaml index 5150221..1ada3ee 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,10 +8,10 @@ services: args: GO_VERSION: "${GO_VERSION:-1.25.0}" MISE_VERSION: "${MISE_VERSION:-v2025.9.10}" - http_proxy: "${http_proxy}" - https_proxy: "${https_proxy}" - image: "ghrc.io/opensourcecorp/oscar:latest" - pull_policy: "if_not_present" + http_proxy: "${http_proxy:-}" + https_proxy: "${https_proxy:-}" + image: "${IMAGE_URI:-}" + pull_policy: "build" command: - "ci" environment: diff --git a/internal/oscarcfg/config.go b/internal/oscarcfg/config.go index fb9c717..90ce386 100644 --- a/internal/oscarcfg/config.go +++ b/internal/oscarcfg/config.go @@ -20,20 +20,33 @@ type Config struct { // Deliverables contains a field for each possible deliverable. type Deliverables struct { - // Defines the GitHub Releases the user wants to create + // See [GoGitHubRelease]. GoGitHubRelease *GoGitHubRelease `yaml:"go_github_release" json:"go_github_release"` + // See [ContainerImage]. + ContainerImage *ContainerImage `yaml:"container_image" json:"container_image"` } // GoGitHubRelease defines the arguments necessary to create GitHub Releases for Go binaries. type GoGitHubRelease struct { + // The target GitHub Repository. Repo string `yaml:"repo" json:"repo"` - // BuildSources are the filepaths to the "main" packages to be built. + // The filepaths to the "main" packages to be built. BuildSources []string `yaml:"build_sources" json:"build_sources"` - // Draft flags whether the Release should be left in Draft state at create-time. This can be - // useful to set if you want to review the Release contents before actually publishing. + // Flags whether the Release should be left in Draft state at create-time. This can be useful to + // set if you want to review the Release contents before actually publishing. Draft bool } +// ContainerImage defines the arguments necessary to build & push container image artifacts. +type ContainerImage struct { + // The target registry provider domain, e.g. "ghcr.io". + Registry string `yaml:"registry" json:"registry"` + // The target OCI repository name, e.g. "oscar". + Owner string `yaml:"owner" json:"owner"` + // The target OCI repository, e.g. "oscar". + Repo string `yaml:"repo" json:"repo"` +} + // Get returns a populated [Config] based on the oscar config file location. If `path` is not // provided, it will default to looking in the calling directory. func Get(pathOverride ...string) (Config, error) { diff --git a/internal/tasks/delivery/run.go b/internal/tasks/delivery/run.go index 160a9c8..1b4b25c 100644 --- a/internal/tasks/delivery/run.go +++ b/internal/tasks/delivery/run.go @@ -10,6 +10,7 @@ import ( "github.com/opensourcecorp/oscar/internal/consts" iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/tasks/ci" + containertools "github.com/opensourcecorp/oscar/internal/tasks/tools/containers" gotools "github.com/opensourcecorp/oscar/internal/tasks/tools/go" taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) @@ -19,7 +20,8 @@ import ( func getDeliveryTaskMap(repo taskutil.Repo) (taskutil.TaskMap, error) { out := make(taskutil.TaskMap) for langName, getTasksFunc := range map[string]func(taskutil.Repo) ([]taskutil.Tasker, error){ - "Go": gotools.NewTasksForDelivery, + "Go": gotools.NewTasksForDelivery, + "OCI Images": containertools.NewTasksForDelivery, // "Python": pytools.NewTasksForDelivery, // "Terraform": tftools.NewTasksForDelivery, // "Markdown": mdtools.NewTasksForDelivery, diff --git a/internal/tasks/tools/containers/deliver.go b/internal/tasks/tools/containers/deliver.go new file mode 100644 index 0000000..b38a6ed --- /dev/null +++ b/internal/tasks/tools/containers/deliver.go @@ -0,0 +1,137 @@ +package containertools + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/opensourcecorp/oscar/internal/oscarcfg" + iprint "github.com/opensourcecorp/oscar/internal/print" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" + "go.yaml.in/yaml/v4" +) + +type ( + imageBuildPush struct{ taskutil.Tool } +) + +type registryMapping struct { + Name string + GitHub struct { + AuthCommand []string + } +} + +func newRegistryMap(username string) registryMapping { + return registryMapping{ + GitHub: struct{ AuthCommand []string }{ + AuthCommand: []string{"bash", "-c", fmt.Sprintf(` + echo ${GITHUB_TOKEN} | docker login ghcr.io --username %s --password-stdin + `, username, + )}, + }, + } +} + +// NewTasksForDelivery returns the list of CI tasks. +func NewTasksForDelivery(repo taskutil.Repo) ([]taskutil.Tasker, error) { + cfg, err := oscarcfg.Get() + if err != nil { + return nil, err + } + + if repo.HasContainerfile && cfg.Deliver != nil { + out := make([]taskutil.Tasker, 0) + + if cfg.Deliver.ContainerImage != nil { + out = append(out, imageBuildPush{}) + } + + return out, nil + } + + return nil, nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t imageBuildPush) InfoText() string { return "Image Build & Push" } + +// Run implements [taskutil.Tasker.Run]. +func (t imageBuildPush) Exec(ctx context.Context) error { + rootCfg, err := oscarcfg.Get() + if err != nil { + return err + } + cfg := rootCfg.Deliver.ContainerImage + + composeFileContents, err := os.ReadFile("docker-compose.yaml") + if err != nil { + return err + } + + composeFile := make(map[string]any) + if err := yaml.Unmarshal(composeFileContents, composeFile); err != nil { + return err + } + iprint.Debugf("composeFile unmarshalled: %#v\n", composeFile) + + // TODO: use a validator package instead, so we can check all the fields more easily + if cfg.Repo == "" { + return fmt.Errorf("required 'repo' key not set for this Deliverable in oscar.yaml") + } + + uri := fmt.Sprintf( + "%s/%s/%s:%s", + cfg.Registry, cfg.Owner, cfg.Repo, rootCfg.Version, + ) + + curDir, err := os.Getwd() + if err != nil { + return err + } + + // GROSS, DUDE + composeFile["services"].(map[string]any)[cfg.Repo].(map[string]any)["image"] = uri + composeFile["services"].(map[string]any)[cfg.Repo].(map[string]any)["build"].(map[string]any)["context"] = curDir + + composeOut, err := yaml.Marshal(composeFile) + if err != nil { + return err + } + + workDir := filepath.Join(os.TempDir(), "oscar-oci") + if err := os.MkdirAll(workDir, 0755); err != nil { + return err + } + + outPath := filepath.Join(workDir, "docker-compose.yaml") + if err := os.WriteFile(outPath, composeOut, 0644); err != nil { + return err + } + + registryMap := newRegistryMap(cfg.Repo) + + var authArgs []string + if strings.Contains(cfg.Registry, "ghcr") { + authArgs = registryMap.GitHub.AuthCommand + } + + if _, err := taskutil.RunCommand(ctx, authArgs); err != nil { + return err + } + + buildPushArgs := []string{"bash", "-c", fmt.Sprintf(` + docker compose --file %s build --push %s + `, outPath, cfg.Repo, + )} + if _, err := taskutil.RunCommand(ctx, buildPushArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t imageBuildPush) Post(_ context.Context) error { return nil } diff --git a/internal/tasks/tools/go/deliver.go b/internal/tasks/tools/go/deliver.go index 2126f6a..8fa526d 100644 --- a/internal/tasks/tools/go/deliver.go +++ b/internal/tasks/tools/go/deliver.go @@ -36,7 +36,7 @@ func NewTasksForDelivery(repo taskutil.Repo) ([]taskutil.Tasker, error) { } // InfoText implements [taskutil.Tasker.InfoText]. -func (t ghRelease) InfoText() string { return "GitHub Releases" } +func (t ghRelease) InfoText() string { return "GitHub Release" } // Exec implements [taskutil.Tasker.Exec]. func (t ghRelease) Exec(ctx context.Context) error { @@ -53,6 +53,8 @@ func (t ghRelease) Exec(ctx context.Context) error { return err } + // TODO: actually write GH Release code + return nil } diff --git a/mise.toml b/mise.toml index 2cd2cce..c6139a1 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,6 @@ [settings] experimental = true +jobs = 4 [tools] github-cli = "prefix:2" diff --git a/oscar.yaml b/oscar.yaml index f850a14..fe48ddd 100644 --- a/oscar.yaml +++ b/oscar.yaml @@ -7,5 +7,6 @@ deliver: - "./cmd/oscar" draft: false container_image: - host: "ghcr.io" - repo: "opensourcecorp/oscar" + registry: "ghcr.io" + owner: "opensourcecorp" + repo: "oscar" From 64da11aab45912b42e89f357ff75f4bb0099e448 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Fri, 19 Sep 2025 21:48:37 -0500 Subject: [PATCH 16/38] Commit a bad YAML file to test git-diff in CI --- .github/workflows/main.yaml | 119 ++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 6f95767..7d37e49 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,5 +1,5 @@ ---- -name: "main" +name: main + on: push: branches: @@ -9,8 +9,10 @@ on: - "opened" - "reopened" - "synchronize" + env: GITHUB_TOKEN: "{{ secrets.GITHUB_TOKEN }}" + jobs: ci: runs-on: "ubuntu-latest" @@ -32,64 +34,65 @@ jobs: cache: true - name: "Run CI Tasks" run: "make ci" - deliver: - permissions: - contents: "read" - packages: "write" - steps: - - name: "Checkout repository" - uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # release v5.0.0 - - name: "Install system packages" - run: | - sudo apt-get update - sudo apt-get install -y \ - make - - name: "Set up mise" - uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # release v3.2.0 - with: - version: "2025.9.10" - install: true # runs `mise install` - cache: true - - name: "Run Delivery Tasks" - run: "make deliver" - # + # deliver: + # runs-on: "ubuntu-latest" + # permissions: + # contents: "read" + # packages: "write" + # steps: + # - name: "Checkout repository" + # uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # release v5.0.0 + # - name: "Install system packages" + # run: | + # sudo apt-get update + # sudo apt-get install -y \ + # make + # - name: "Set up mise" + # uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # release v3.2.0 + # with: + # version: "2025.9.10" + # install: true # runs `mise install` + # cache: true + # - name: "Run Delivery Tasks" + # run: "make deliver" + # - name: Set up Docker Buildx # uses: docker/setup-buildx-action@v2 -# - name: Log in to GHCR -# uses: docker/login-action@v2 -# with: -# registry: ghcr.io -# username: ${{ github.repository_owner }} -# password: ${{ secrets.GITHUB_TOKEN }} + # - name: Log in to GHCR + # uses: docker/login-action@v2 + # with: + # registry: ghcr.io + # username: ${{ github.repository_owner }} + # password: ${{ secrets.GITHUB_TOKEN }} -# # Generate any tags we want for the images -# # (https://github.com/docker/build-push-action/blob/master/docs/advanced/tags-labels.md) -# - name: Extract metadata (tags, labels) for OCI image -# id: meta -# uses: docker/metadata-action@v4 -# with: -# images: ghcr.io/opensourcecorp/oscar -# tags: | -# type=sha + # # Generate any tags we want for the images + # # (https://github.com/docker/build-push-action/blob/master/docs/advanced/tags-labels.md) + # - name: Extract metadata (tags, labels) for OCI image + # id: meta + # uses: docker/metadata-action@v4 + # with: + # images: ghcr.io/opensourcecorp/oscar + # tags: | + # type=sha -# # non-mainline -# - name: Build and push non-mainline OCI image -# if: github.ref != 'refs/heads/main' -# uses: docker/build-push-action@v3 -# with: -# context: . -# file: Containerfile -# push: true -# tags: -# - "latest" -# - "${VERSION}" + # # non-mainline + # - name: Build and push non-mainline OCI image + # if: github.ref != 'refs/heads/main' + # uses: docker/build-push-action@v3 + # with: + # context: . + # file: Containerfile + # push: true + # tags: + # - "latest" + # - "${VERSION}" -# # mainline -# - name: Build and push mainline OCI image -# if: github.ref == 'refs/heads/main' -# uses: docker/build-push-action@v3 -# with: -# context: . -# file: Containerfile -# push: true -# tags: ghcr.io/opensourcecorp/oscar:latest + # # mainline + # - name: Build and push mainline OCI image + # if: github.ref == 'refs/heads/main' + # uses: docker/build-push-action@v3 + # with: + # context: . + # file: Containerfile + # push: true + # tags: ghcr.io/opensourcecorp/oscar:latest From 251d60867f46ff0aa3d79463fbd6dea288dfeaf2 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 00:07:53 -0500 Subject: [PATCH 17/38] jfc i dont even know how many changes were in here --- .github/workflows/main.yaml | 3 +- CONTRIBUTING.md | 31 +- Makefile | 28 +- README.md | 64 ++-- go.mod | 22 +- go.sum | 43 ++- internal/cli/root.go | 2 +- .../oscar/config/v1/config.pb.go | 340 ++++++++++++++++++ internal/git/ci.go | 43 +-- internal/git/deliver.go | 51 --- internal/git/doc.go | 4 +- internal/git/git.go | 108 ++++++ internal/git/git_test.go | 39 ++ internal/oscarcfg/config.go | 82 ++--- internal/oscarcfg/config_test.go | 28 +- internal/oscarcfg/test.oscar.yaml | 9 +- internal/semver/doc.go | 5 - internal/semver/semver.go | 72 ---- internal/semver/semver_test.go | 55 --- internal/tasks/ci/run.go | 14 +- internal/tasks/tools/containers/deliver.go | 57 ++- .../tasks/tools/containers/deliver_test.go | 32 ++ internal/tasks/tools/go/deliver.go | 6 +- internal/tasks/tools/toolcfg/.yamlfmt | 1 + internal/tasks/tools/toolcfg/.yamllint | 3 + internal/tasks/tools/toolcfg/staticcheck.conf | 2 + internal/tasks/tools/version/ci.go | 7 +- internal/tasks/tools/version/ci_test.go | 11 +- mise.toml | 46 +-- oscar.yaml | 3 +- proto/buf.gen.yaml | 12 + proto/buf.lock | 6 + proto/buf.yaml | 18 + proto/generate.go | 11 + .../oscar/config/v1/config.proto | 55 +++ 35 files changed, 907 insertions(+), 406 deletions(-) create mode 100644 internal/generated/opensourcecorp/oscar/config/v1/config.pb.go delete mode 100644 internal/git/deliver.go create mode 100644 internal/git/git.go create mode 100644 internal/git/git_test.go delete mode 100644 internal/semver/doc.go delete mode 100644 internal/semver/semver.go delete mode 100644 internal/semver/semver_test.go create mode 100644 internal/tasks/tools/containers/deliver_test.go create mode 100644 proto/buf.gen.yaml create mode 100644 proto/buf.lock create mode 100644 proto/buf.yaml create mode 100644 proto/generate.go create mode 100644 proto/opensourcecorp/oscar/config/v1/config.proto diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 7d37e49..1302294 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,4 +1,5 @@ -name: main +--- +name: "main" on: push: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4348cd5..e32f5ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,15 +2,19 @@ ## Workstation setup -`oscar`'s development leans heavily into using the root-level Makefile. +`oscar`'s development is the most consistent if you use the following: -Development of `oscar` is the most consistent if your workstation has [`mise`](https://mise.jdx.dev) -available on your `$PATH`. However, the Makefile's targets are configured to also allow for working -with most tooling natively, such as Go, if they are available. +* The root-level Makefile. +* [`mise`](https://mise.jdx.dev), via the root-level `mise.toml`. + +While not strictly required, you will have a much better time if you leverage those. However, the +Makefile's targets are configured to also allow for working with most tooling natively, such as Go, +if they are available. `mise` does not manage Docker Engine, however, so for targets like `make image` you will need to -ensure that you have a container runtime available (like Docker or Podman, overridable via the -`DOCKER` Make variable). +ensure that you have a container runtime available (like the ones for Docker or Podman). The default +command the Makefile tries to run is `docker`, which you can override via the `DOCKER` Make +variable. ## Contribution philosophy @@ -18,8 +22,17 @@ ensure that you have a container runtime available (like Docker or Podman, overr * As a reminder, `oscar`'s runtime behavior is ***intentionally designed to be rigid***. If there is a language or tool you would like to see added, then those contributions are welcome. Fundamental - changes to how `oscar` intends to operate, e.g. allowing for wholesale override of various - linters' settings, are not. + changes to how `oscar` intends to operate, e.g. allowing for user override of a linter's + line-length checks, or which directory `oscar` builds binaries into, are not. -* `hEy tHeRe'S a LoT oF ShElL cOdE iN hErE?!` -- Correct. "Shelling out" is an intentional design +* `hEy tHeRe'S a LoT oF sHeLL cOdE iN hErE?!` -- Correct. "Shelling out" is an intentional design decision, and `oscar` calls out `bash` as a dependency. + +## Adding support for a new Tool + +To add a new Tool to `oscar`, it must: + +* Define a non-exported struct, which embeds `taskutil.Tool`. +* That struct must implement the `taskutil.Tasker` interface. + +See examples across the various `internal/tasks/tools/{lang}/*.go` files. diff --git a/Makefile b/Makefile index 071c1ab..f80a9e6 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ DOCKER ?= docker export IMAGE_REGISTRY ?= ghcr.io export IMAGE_REGISTRY_OWNER ?= opensourcecorp export IMAGE_NAME ?= $(BINNAME) -export IMAGE_TAG ?= 'latest' +export IMAGE_TAG ?= latest export IMAGE_URI ?= $(IMAGE_REGISTRY)/$(IMAGE_REGISTRY_OWNER)/$(IMAGE_NAME):$(IMAGE_TAG) SHELL = /usr/bin/env bash -euo pipefail @@ -26,6 +26,9 @@ FORCE: ci: clean @$(RUN) go run ./cmd/$(BINNAME)/main.go ci +deliver: + @$(RUN) go run ./cmd/$(BINNAME)/main.go deliver + # test is just an alias for ci test: ci @@ -34,19 +37,7 @@ test: ci build: FORCE @$(RUN) go build -o ./build/oscar ./cmd/oscar -ci-container: - @$(DOCKER) build \ - --build-arg http_proxy="$${http_proxy}" \ - --build-arg https_proxy="$${https_proxy}" \ - --build-arg GO_VERSION="$$(awk '/^go/ { print $$2 }' go.mod)" \ - -f ./Containerfile \ - -t $(BINNAME)-test:latest \ - . - -deliver: - @$(RUN) go run ./cmd/$(BINNAME)/main.go deliver - -clean: +clean: FORCE @rm -rf \ /tmp/$(BINNAME)-tests \ ./*cache* \ @@ -60,7 +51,10 @@ clean: image: clean @export BUILDKIT_PROGRESS=plain && \ export GO_VERSION="$$(awk '/^go/ { print $$2 }' go.mod)" && \ - $(DOCKER) compose build + $(RUN) $(DOCKER) compose build + +run-image: FORCE + @$(RUN) $(DOCKER) compose run $(BINNAME) -run-image: - @$(DOCKER) compose run $(BINNAME) +generate: FORCE + @cd ./proto && $(RUN) buf generate diff --git a/README.md b/README.md index 1fa7c62..a28a6ba 100644 --- a/README.md +++ b/README.md @@ -4,34 +4,19 @@ ![Github Actions](https://github.com/opensourcecorp/oscar/actions/workflows/main.yaml/badge.svg) -`oscar` ("OpenSourceCorp Automation Runner") is a highly-opinionated, out-of-the-box task runner. -Originally designed for use exclusively within [OpenSourceCorp's CI/CD -subsystem](https://github.com/opensourcecorp/osc-infra/tree/main/cicd), it is perfectly usable -outside of OSC as well. +`oscar` ("OpenSourceCorp Automation Runner") is a highly-opinionated, out-of-the-box task runner +designed for use across OSC. -`oscar` is "highly-opinionated" in that `oscar` is designed to do each thing ***one single way***. -No choosing what linters to run or how to configure them, no picking which annual flavor of Python -or Nodejs packaging tool, etc. -- `oscar` is built to be ***the*** authoritative toolset for entire -teams and their codebases. +`oscar` is "highly-opinionated" in that it is designed to do each thing ***one single way***. No +choosing what linters to run or how to configure them, no picking which annual flavor of Python or +Nodejs packaging tool, no discrepancies in how to cut & deploy releases, etc. -- `oscar` is built to +be ***the*** authoritative toolset for entire teams and their codebases. -Under the hood, `oscar` uses the excellent [`mise`](https://mise.jdx.dev) quite heavily, and would -like to thank the author & contributors for making something like `oscar` possible without a lot of -wheel-reinvention. - -## How to use - -Before getting started, note that `oscar` has a few host-system runtime dependencies. Some of these -may someday be replaced natively in the future, but some are integral to how `oscar` works -internally. - -* `bash` (version 4.4+) -* `git` +## Features You run oscar by providing it a subcommand, such as `ci`. You can see the full available subcommand list via `oscar --help`. -## Features - | Feature | `oscar` command | Details | | :--------------------- | :-------------- | :--------------------------------- | | Continuous integration | `oscar ci` | [section](#continuous-integration) | @@ -61,25 +46,50 @@ However, this does not mean that someone is prevented from adding *additional* c TODO -| Artifact types | Targets | -| :------------- | :-------------- | -| Go binaries | GitHub Releases | +| Artifact types | Targets | `oscar.yaml` field | +| :--------------- | :--------------- | :------------------------------- | +| Go binaries | GitHub Releases | `deliverables.go_github_release` | +| Container images | Any OCI registry | `deliverables.container_image` | +## Requirements + +Before getting started, note that `oscar` has a few host-system runtime dependencies. Some of these +may someday be replaced natively in the future, but some are integral to how `oscar` works +internally. + +* `bash` (version 4.4+) +* GNU `coreutils` +* `git` + +In addition, some components of `oscar` may require additional host-system tools (e.g. a container +runtime like Docker for building & pushing container images). + +If you are running on macOS, you should be able to install any missing tools via `brew install`-ing +the above by name -- but make sure your `$PATH` is pointing to the correct ones and not the default +BSD-equivalents. + ## Supported platforms -`oscar` is designed to run on Linux, and should work on macOS as well. Native Windows has not been -tested, and is unlikely to work. If you are on a Windows machine, you can run `oscar` in a WSL2 +`oscar` is designed to run on Linux, and should mostly work on macOS as well. Native Windows has not +been tested, and is unlikely to work. If you are on a Windows machine, you can run `oscar` in a WSL2 environment and it will work the same as on Linux. ## Development & Contributions Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details about developing `oscar`. +## Acknowledgements + +Under the hood, `oscar` uses the excellent [`mise`](https://mise.jdx.dev) quite heavily, and would +like to thank the author & contributors for making something like `oscar` possible without a lot of +wheel-reinvention. + ## Roadmap +* Add `oscar.yaml` generator * Add check for changelog Markdown file that matches `oscar.yaml:version` (we should also use that file as the exact GH Release post contents) * Workstation setup diff --git a/go.mod b/go.mod index 5735a9d..1cf2c17 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,24 @@ require ( golang.org/x/mod v0.27.0 ) -require go.yaml.in/yaml/v4 v4.0.0-rc.2 +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 + buf.build/go/protovalidate v1.0.0 + github.com/stretchr/testify v1.11.1 + go.yaml.in/yaml/v4 v4.0.0-rc.2 + google.golang.org/protobuf v1.36.9 +) + +require ( + cel.dev/expr v0.24.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/cel-go v0.26.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index d5599e6..db49db2 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,53 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 h1:DQLS/rRxLHuugVzjJU5AvOwD57pdFl9he/0O7e5P294= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1/go.mod h1:aY3zbkNan5F+cGm9lITDP6oxJIwu0dn9KjJuJjWaHkg= +buf.build/go/protovalidate v1.0.0 h1:IAG1etULddAy93fiBsFVhpj7es5zL53AfB/79CVGtyY= +buf.build/go/protovalidate v1.0.0/go.mod h1:KQmEUrcQuC99hAw+juzOEAmILScQiKBP1Oc36vvCLW8= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cli/root.go b/internal/cli/root.go index ff427fd..8b5d89e 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -72,7 +72,7 @@ func maybeSetDebug(cmd *cli.Command) { func getVersion() (string, error) { cfg, err := oscarcfg.Get() if err != nil { - return "", fmt.Errorf("internal error trying to read oscar config file: %w", err) + return "", fmt.Errorf("reading oscar config file: %w", err) } return cfg.Version, nil diff --git a/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go b/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go new file mode 100644 index 0000000..2de1088 --- /dev/null +++ b/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go @@ -0,0 +1,340 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc (unknown) +// source: opensourcecorp/oscar/config/v1/config.proto + +package oscarcfgpbv1 + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Config defines the top-level structure of oscar's config file. +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Version is the version string for the codebase. Must be a valid Semantic Version string. + // + // Example: "1.0.0" + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + // Deliverables is the collection of possible deliverable artifacts. + Deliverables *Deliverables `protobuf:"bytes,2,opt,name=deliverables,proto3" json:"deliverables,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_opensourcecorp_oscar_config_v1_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_opensourcecorp_oscar_config_v1_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_opensourcecorp_oscar_config_v1_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Config) GetDeliverables() *Deliverables { + if x != nil { + return x.Deliverables + } + return nil +} + +// Deliverables contains a field for each possible deliverable. +type Deliverables struct { + state protoimpl.MessageState `protogen:"open.v1"` + // See [GoGitHubRelease]. + GoGithubRelease *GoGitHubRelease `protobuf:"bytes,1,opt,name=go_github_release,json=goGithubRelease,proto3" json:"go_github_release,omitempty"` + // See [ContainerImage]. + ContainerImage *ContainerImage `protobuf:"bytes,2,opt,name=container_image,json=containerImage,proto3" json:"container_image,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Deliverables) Reset() { + *x = Deliverables{} + mi := &file_opensourcecorp_oscar_config_v1_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Deliverables) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Deliverables) ProtoMessage() {} + +func (x *Deliverables) ProtoReflect() protoreflect.Message { + mi := &file_opensourcecorp_oscar_config_v1_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Deliverables.ProtoReflect.Descriptor instead. +func (*Deliverables) Descriptor() ([]byte, []int) { + return file_opensourcecorp_oscar_config_v1_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Deliverables) GetGoGithubRelease() *GoGitHubRelease { + if x != nil { + return x.GoGithubRelease + } + return nil +} + +func (x *Deliverables) GetContainerImage() *ContainerImage { + if x != nil { + return x.ContainerImage + } + return nil +} + +// GoGitHubRelease defines the arguments necessary to create GitHub Releases for Go binaries. +type GoGitHubRelease struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The filepaths to the "main" packages to be built. + // + // Example: - "./cmd/oscar" + BuildSources []string `protobuf:"bytes,1,rep,name=build_sources,json=buildSources,proto3" json:"build_sources,omitempty"` + // Optionally flags whether the Release should be left in Draft state at create-time. This can + // be useful to set if you want to review the Release contents before actually publishing. + // + // Example: false + Draft bool `protobuf:"varint,2,opt,name=draft,proto3" json:"draft,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GoGitHubRelease) Reset() { + *x = GoGitHubRelease{} + mi := &file_opensourcecorp_oscar_config_v1_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GoGitHubRelease) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GoGitHubRelease) ProtoMessage() {} + +func (x *GoGitHubRelease) ProtoReflect() protoreflect.Message { + mi := &file_opensourcecorp_oscar_config_v1_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GoGitHubRelease.ProtoReflect.Descriptor instead. +func (*GoGitHubRelease) Descriptor() ([]byte, []int) { + return file_opensourcecorp_oscar_config_v1_config_proto_rawDescGZIP(), []int{2} +} + +func (x *GoGitHubRelease) GetBuildSources() []string { + if x != nil { + return x.BuildSources + } + return nil +} + +func (x *GoGitHubRelease) GetDraft() bool { + if x != nil { + return x.Draft + } + return false +} + +// ContainerImage defines the arguments necessary to build & push container image artifacts. +type ContainerImage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The target registry provider domain. + // + // Example: "ghcr.io" + Registry string `protobuf:"bytes,1,opt,name=registry,proto3" json:"registry,omitempty"` + // The target OCI repository name. + // + // Example: "opensourcecorp" + Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + // The target OCI repository. May contain as many subpaths to the actual image artifact as + // necessary based on the registry, e.g. "my-repo/my-image-group/my-image". + // + // Example: "oscar" + Repo string `protobuf:"bytes,3,opt,name=repo,proto3" json:"repo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContainerImage) Reset() { + *x = ContainerImage{} + mi := &file_opensourcecorp_oscar_config_v1_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContainerImage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContainerImage) ProtoMessage() {} + +func (x *ContainerImage) ProtoReflect() protoreflect.Message { + mi := &file_opensourcecorp_oscar_config_v1_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContainerImage.ProtoReflect.Descriptor instead. +func (*ContainerImage) Descriptor() ([]byte, []int) { + return file_opensourcecorp_oscar_config_v1_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ContainerImage) GetRegistry() string { + if x != nil { + return x.Registry + } + return "" +} + +func (x *ContainerImage) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *ContainerImage) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +var File_opensourcecorp_oscar_config_v1_config_proto protoreflect.FileDescriptor + +const file_opensourcecorp_oscar_config_v1_config_proto_rawDesc = "" + + "\n" + + "+opensourcecorp/oscar/config/v1/config.proto\x12\x1eopensourcecorp.oscar.config.v1\x1a\x1bbuf/validate/validate.proto\"\xb6\x01\n" + + "\x06Config\x12Z\n" + + "\aversion\x18\x01 \x01(\tB@\xbaH=r;29^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9]+)?(\\+[a-zA-Z0-9]+)?$R\aversion\x12P\n" + + "\fdeliverables\x18\x02 \x01(\v2,.opensourcecorp.oscar.config.v1.DeliverablesR\fdeliverables\"\xc4\x01\n" + + "\fDeliverables\x12[\n" + + "\x11go_github_release\x18\x01 \x01(\v2/.opensourcecorp.oscar.config.v1.GoGitHubReleaseR\x0fgoGithubRelease\x12W\n" + + "\x0fcontainer_image\x18\x02 \x01(\v2..opensourcecorp.oscar.config.v1.ContainerImageR\x0econtainerImage\"T\n" + + "\x0fGoGitHubRelease\x12+\n" + + "\rbuild_sources\x18\x01 \x03(\tB\x06\xbaH\x03\xc8\x01\x01R\fbuildSources\x12\x14\n" + + "\x05draft\x18\x02 \x01(\bR\x05draft\"n\n" + + "\x0eContainerImage\x12\"\n" + + "\bregistry\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bregistry\x12\x1c\n" + + "\x05owner\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x05owner\x12\x1a\n" + + "\x04repo\x18\x03 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x04repoB`Z^github.com/opensourcecorp/oscar/internal/generated/opensourcecorp/oscar/config/v1;oscarcfgpbv1b\x06proto3" + +var ( + file_opensourcecorp_oscar_config_v1_config_proto_rawDescOnce sync.Once + file_opensourcecorp_oscar_config_v1_config_proto_rawDescData []byte +) + +func file_opensourcecorp_oscar_config_v1_config_proto_rawDescGZIP() []byte { + file_opensourcecorp_oscar_config_v1_config_proto_rawDescOnce.Do(func() { + file_opensourcecorp_oscar_config_v1_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_opensourcecorp_oscar_config_v1_config_proto_rawDesc), len(file_opensourcecorp_oscar_config_v1_config_proto_rawDesc))) + }) + return file_opensourcecorp_oscar_config_v1_config_proto_rawDescData +} + +var file_opensourcecorp_oscar_config_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_opensourcecorp_oscar_config_v1_config_proto_goTypes = []any{ + (*Config)(nil), // 0: opensourcecorp.oscar.config.v1.Config + (*Deliverables)(nil), // 1: opensourcecorp.oscar.config.v1.Deliverables + (*GoGitHubRelease)(nil), // 2: opensourcecorp.oscar.config.v1.GoGitHubRelease + (*ContainerImage)(nil), // 3: opensourcecorp.oscar.config.v1.ContainerImage +} +var file_opensourcecorp_oscar_config_v1_config_proto_depIdxs = []int32{ + 1, // 0: opensourcecorp.oscar.config.v1.Config.deliverables:type_name -> opensourcecorp.oscar.config.v1.Deliverables + 2, // 1: opensourcecorp.oscar.config.v1.Deliverables.go_github_release:type_name -> opensourcecorp.oscar.config.v1.GoGitHubRelease + 3, // 2: opensourcecorp.oscar.config.v1.Deliverables.container_image:type_name -> opensourcecorp.oscar.config.v1.ContainerImage + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_opensourcecorp_oscar_config_v1_config_proto_init() } +func file_opensourcecorp_oscar_config_v1_config_proto_init() { + if File_opensourcecorp_oscar_config_v1_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_opensourcecorp_oscar_config_v1_config_proto_rawDesc), len(file_opensourcecorp_oscar_config_v1_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_opensourcecorp_oscar_config_v1_config_proto_goTypes, + DependencyIndexes: file_opensourcecorp_oscar_config_v1_config_proto_depIdxs, + MessageInfos: file_opensourcecorp_oscar_config_v1_config_proto_msgTypes, + }.Build() + File_opensourcecorp_oscar_config_v1_config_proto = out.File + file_opensourcecorp_oscar_config_v1_config_proto_goTypes = nil + file_opensourcecorp_oscar_config_v1_config_proto_depIdxs = nil +} diff --git a/internal/git/ci.go b/internal/git/ci.go index 3b3b97a..251d054 100644 --- a/internal/git/ci.go +++ b/internal/git/ci.go @@ -1,4 +1,4 @@ -package git +package igit import ( "context" @@ -8,7 +8,6 @@ import ( "strings" iprint "github.com/opensourcecorp/oscar/internal/print" - taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) // CI defines metadata & behavior for CI tasks. @@ -20,12 +19,6 @@ type CI struct { CurrentStatus Status } -// Status holds various pieces of information about Git status. -type Status struct { - Diff []string - UntrackedFiles []string -} - // NewForCI returns Git information for CI tasks. func NewForCI(ctx context.Context) (*CI, error) { status, err := getRawStatus(ctx) @@ -50,7 +43,7 @@ func (g *CI) Update(ctx context.Context) error { for _, line := range status.Diff { if !slices.Contains(g.BaselineStatus.Diff, line) { - filename := regexp.MustCompile(`^ [A-Z] `).ReplaceAllString(line, "") + filename := regexp.MustCompile(`^ ?[A-Z]+ `).ReplaceAllString(line, "") diff = append(diff, filename) } } @@ -89,38 +82,6 @@ func (g *CI) StatusHasChanged(ctx context.Context) (bool, error) { return statusChanged, nil } -// getRawStatus returns a slightly-modified "git status" output, so that calling tools can parse it -// more easily. -func getRawStatus(ctx context.Context) (Status, error) { - outputBytes, err := taskutil.RunCommand(ctx, []string{"git", "status", "--porcelain"}) - if err != nil { - return Status{}, fmt.Errorf("getting git status output: %w", err) - } - - output := string(outputBytes) - outputSplit := strings.Split(output, "\n") - - untrackedFiles := make([]string, 0) - diff := make([]string, 0) - for _, line := range outputSplit { - if line == "" { - continue - } - if strings.HasPrefix(line, "??") { - filename := strings.ReplaceAll(line, "?? ", "") - untrackedFiles = append(untrackedFiles, filename) - } else { - filename := regexp.MustCompile(`^( +)?[A-Z]+ +`).ReplaceAllString(line, "") - diff = append(diff, filename) - } - } - - return Status{ - Diff: diff, - UntrackedFiles: untrackedFiles, - }, nil -} - // updateStatus updates the tracked Git status so that it can be compared against the baseline. func (g *CI) updateStatus(ctx context.Context) error { // So any future debug logs have a line break in them diff --git a/internal/git/deliver.go b/internal/git/deliver.go deleted file mode 100644 index 2c632cc..0000000 --- a/internal/git/deliver.go +++ /dev/null @@ -1,51 +0,0 @@ -package git - -import ( - "context" - "fmt" - - "github.com/opensourcecorp/oscar/internal/oscarcfg" - iprint "github.com/opensourcecorp/oscar/internal/print" - taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" -) - -// Delivery defines metadata & behavior for Delivery tasks. -type Delivery struct { - Root string - LatestTag string - // From oscar config file - CurrentVersion string -} - -// NewForDelivery returns Git information for Delivery tasks. -func NewForDelivery(ctx context.Context) (*Delivery, error) { - root, err := taskutil.RunCommand(ctx, []string{"git", "rev-parse", "--show-toplevel"}) - if err != nil { - return nil, err - } - - latestTag, err := taskutil.RunCommand(ctx, []string{"bash", "-c", "git tag --list | tail -n1"}) - if err != nil { - return nil, err - } - iprint.Debugf("latest Git tag: '%s'\n", latestTag) - - cfg, err := oscarcfg.Get() - if err != nil { - return nil, fmt.Errorf("getting oscar config: %w", err) - } - version := cfg.Version - - if version == "" { - return nil, fmt.Errorf("could not determine a Semantic Version from your oscar config file") - } - - out := &Delivery{ - Root: root, - LatestTag: latestTag, - CurrentVersion: version, - } - iprint.Debugf("git.Delivery: %+v\n", out) - - return out, nil -} diff --git a/internal/git/doc.go b/internal/git/doc.go index 00b2819..cdc1b10 100644 --- a/internal/git/doc.go +++ b/internal/git/doc.go @@ -1,2 +1,2 @@ -// Package git provides interoperability with Git. -package git +// Package igit provides interoperability with Git. +package igit diff --git a/internal/git/git.go b/internal/git/git.go new file mode 100644 index 0000000..e30c3d1 --- /dev/null +++ b/internal/git/git.go @@ -0,0 +1,108 @@ +package igit + +import ( + "context" + "fmt" + "regexp" + "strings" + + iprint "github.com/opensourcecorp/oscar/internal/print" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" +) + +type Git struct { + Root string + Branch string + LatestTag string + LatestCommit string + IsDirty bool +} + +// Status holds various pieces of information about Git status. +type Status struct { + Diff []string + UntrackedFiles []string +} + +func New(ctx context.Context) (*Git, error) { + root, err := taskutil.RunCommand(ctx, []string{"git", "rev-parse", "--show-toplevel"}) + if err != nil { + return nil, err + } + iprint.Debugf("Git root on host: '%s'\n", root) + + branch, err := taskutil.RunCommand(ctx, []string{"git", "rev-parse", "--abbrev-ref", "HEAD"}) + if err != nil { + return nil, err + } + iprint.Debugf("Git branch: '%s'\n", branch) + + latestTag, err := taskutil.RunCommand(ctx, []string{"bash", "-c", "git tag --list | tail -n1"}) + if err != nil { + return nil, err + } + iprint.Debugf("latest Git tag: '%s'\n", latestTag) + + latestCommit, err := taskutil.RunCommand(ctx, []string{"git", "rev-parse", "--short=8", "HEAD"}) + if err != nil { + return nil, err + } + iprint.Debugf("latest Git commit: '%s'\n", latestCommit) + + gitStatus, err := getRawStatus(ctx) + if err != nil { + return nil, fmt.Errorf("getting Git status: %w", err) + } + + var isDirty bool + if len(gitStatus.Diff) > 0 || len(gitStatus.UntrackedFiles) > 0 { + isDirty = true + } + + out := Git{ + Root: root, + Branch: branch, + LatestTag: latestTag, + LatestCommit: latestCommit, + IsDirty: isDirty, + } + iprint.Debugf("Git: %+v\n", out) + + return &out, nil +} + +func (g *Git) SanitizedBranch() string { + return regexp.MustCompile(`[_/]`).ReplaceAllString(g.Branch, "-") +} + +// getRawStatus returns a slightly-modified "git status" output, so that calling tools can parse it +// more easily. +func getRawStatus(ctx context.Context) (Status, error) { + outputBytes, err := taskutil.RunCommand(ctx, []string{"git", "status", "--porcelain"}) + if err != nil { + return Status{}, fmt.Errorf("getting git status output: %w", err) + } + + output := string(outputBytes) + outputSplit := strings.Split(output, "\n") + + untrackedFiles := make([]string, 0) + diff := make([]string, 0) + for _, line := range outputSplit { + if line == "" { + continue + } + if strings.HasPrefix(line, "??") { + filename := strings.ReplaceAll(line, "?? ", "") + untrackedFiles = append(untrackedFiles, filename) + } else { + filename := regexp.MustCompile(`^( +)?[A-Z]+ +`).ReplaceAllString(line, "") + diff = append(diff, filename) + } + } + + return Status{ + Diff: diff, + UntrackedFiles: untrackedFiles, + }, nil +} diff --git a/internal/git/git_test.go b/internal/git/git_test.go new file mode 100644 index 0000000..588633a --- /dev/null +++ b/internal/git/git_test.go @@ -0,0 +1,39 @@ +package igit + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBranchForURI(t *testing.T) { + want := "feature-wip" + + tt := []struct { + Name string + Branch string + }{ + { + Name: "no replacement", + Branch: "feature-wip", + }, + { + Name: "replace slashes", + Branch: "feature/wip", + }, + { + Name: "replace underscores", + Branch: "feature_wip", + }, + } + + for _, s := range tt { + t.Run(s.Name, func(t *testing.T) { + g := &Git{ + Branch: s.Branch, + } + got := g.SanitizedBranch() + assert.Equal(t, want, got) + }) + } +} diff --git a/internal/oscarcfg/config.go b/internal/oscarcfg/config.go index 90ce386..646a6c4 100644 --- a/internal/oscarcfg/config.go +++ b/internal/oscarcfg/config.go @@ -1,55 +1,22 @@ package oscarcfg import ( + "encoding/json" "fmt" "os" + "buf.build/go/protovalidate" "github.com/opensourcecorp/oscar/internal/consts" + oscarcfgpbv1 "github.com/opensourcecorp/oscar/internal/generated/opensourcecorp/oscar/config/v1" iprint "github.com/opensourcecorp/oscar/internal/print" "go.yaml.in/yaml/v4" + "golang.org/x/mod/semver" + "google.golang.org/protobuf/encoding/protojson" ) -// Config defines the top-level structure of oscar's config file. -type Config struct { - // Version is the version string for the codebase. - Version string `yaml:"version" json:"version"` - // Deliver is the collection of possible deliverable artifacts. - Deliver *Deliverables `yaml:"deliver" json:"deliver"` - // Deploy Deployables `yaml:"deploy" json:"deploy"` -} - -// Deliverables contains a field for each possible deliverable. -type Deliverables struct { - // See [GoGitHubRelease]. - GoGitHubRelease *GoGitHubRelease `yaml:"go_github_release" json:"go_github_release"` - // See [ContainerImage]. - ContainerImage *ContainerImage `yaml:"container_image" json:"container_image"` -} - -// GoGitHubRelease defines the arguments necessary to create GitHub Releases for Go binaries. -type GoGitHubRelease struct { - // The target GitHub Repository. - Repo string `yaml:"repo" json:"repo"` - // The filepaths to the "main" packages to be built. - BuildSources []string `yaml:"build_sources" json:"build_sources"` - // Flags whether the Release should be left in Draft state at create-time. This can be useful to - // set if you want to review the Release contents before actually publishing. - Draft bool -} - -// ContainerImage defines the arguments necessary to build & push container image artifacts. -type ContainerImage struct { - // The target registry provider domain, e.g. "ghcr.io". - Registry string `yaml:"registry" json:"registry"` - // The target OCI repository name, e.g. "oscar". - Owner string `yaml:"owner" json:"owner"` - // The target OCI repository, e.g. "oscar". - Repo string `yaml:"repo" json:"repo"` -} - // Get returns a populated [Config] based on the oscar config file location. If `path` is not // provided, it will default to looking in the calling directory. -func Get(pathOverride ...string) (Config, error) { +func Get(pathOverride ...string) (*oscarcfgpbv1.Config, error) { path := consts.DefaultOscarCfgFileName // Handle the override so we can test this function, and use it in other ways (like checking the @@ -58,16 +25,41 @@ func Get(pathOverride ...string) (Config, error) { path = pathOverride[0] } - data, err := os.ReadFile(path) + yamlData, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading oscar config file: %w", err) + } + iprint.Debugf("data read from oscar config file:\n%s\n", string(yamlData)) + + jsonSweepMap := make(map[string]any) + if err := yaml.Unmarshal(yamlData, jsonSweepMap); err != nil { + panic(err) + } + iprint.Debugf("YAML data unmarshalled to map: %+v\n", jsonSweepMap) + + jsonData, err := json.Marshal(jsonSweepMap) if err != nil { - return Config{}, fmt.Errorf("reading oscar config file: %w", err) + panic(err) } - iprint.Debugf("data read from oscar config file: %s\n", string(data)) + iprint.Debugf("map data as JSON string: %s\n", string(jsonData)) - var cfg Config - if err := yaml.Unmarshal(data, &cfg); err != nil { - return Config{}, fmt.Errorf("unmarshalling oscar config file '%s': %w", path, err) + var cfg = &oscarcfgpbv1.Config{} + if err := protojson.Unmarshal(jsonData, cfg); err != nil { + return nil, fmt.Errorf("unmarshalling oscar config file '%s': %w", path, err) + } + iprint.Debugf("proto message: %+v\n", cfg) + + if err := protovalidate.Validate(cfg); err != nil { + return nil, fmt.Errorf("validating oscar config file '%s': %w", path, err) } return cfg, nil } + +// VersionHasBeenIncremented reports whether the newVersion is greater than the oldVersion. +func VersionHasBeenIncremented(newVersion string, oldVersion string) bool { + compValue := semver.Compare("v"+newVersion, "v"+oldVersion) + iprint.Debugf("semver comparison value: %d\n", compValue) + + return compValue > 0 +} diff --git a/internal/oscarcfg/config_test.go b/internal/oscarcfg/config_test.go index fa06511..e48f18a 100644 --- a/internal/oscarcfg/config_test.go +++ b/internal/oscarcfg/config_test.go @@ -2,28 +2,28 @@ package oscarcfg import ( "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestRead(t *testing.T) { - cfg, err := Get("test.oscar.yaml") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } +const testConfigFilePath = "test.oscar.yaml" + +func TestGet(t *testing.T) { + cfg, err := Get(testConfigFilePath) + require.NoError(t, err) - t.Logf("parsed cfg: %+v", cfg) + t.Logf("parsed cfg:\n< %+v >", cfg) t.Run("version", func(t *testing.T) { want := "1.0.0" - if cfg.Version != want { - t.Errorf("version: wanted '%s', got '%s'", want, cfg.Version) - } + assert.Equal(t, want, cfg.GetVersion()) }) t.Run("deliver", func(t *testing.T) { - wantGHRelease := "test" - gotGHRelease := cfg.Deliver.GoGitHubRelease.Repo - if gotGHRelease != wantGHRelease { - t.Errorf("GH Release: wanted '%s', got '%s'", wantGHRelease, gotGHRelease) - } + wantBuildSources := []string{"./cmd/test"} + gotBuildSources := cfg.GetDeliverables().GetGoGithubRelease().GetBuildSources() + + assert.Equal(t, wantBuildSources, gotBuildSources) }) } diff --git a/internal/oscarcfg/test.oscar.yaml b/internal/oscarcfg/test.oscar.yaml index 3e20920..bf2a63d 100644 --- a/internal/oscarcfg/test.oscar.yaml +++ b/internal/oscarcfg/test.oscar.yaml @@ -1,10 +1,11 @@ --- version: "1.0.0" -deliver: - go_binaries: - - build_source: "./cmd/test" +deliverables: go_github_release: - repo: "test" build_sources: - "./cmd/test" draft: false + container_image: + registry: "ghcr.io" + owner: "opensourcecorp" + repo: "oscar" diff --git a/internal/semver/doc.go b/internal/semver/doc.go deleted file mode 100644 index 583bab4..0000000 --- a/internal/semver/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package semver provides tooling for working with version information, in a way that conforms with -// the [Semantic Versioning] approach. -// -// [Semantic Versioning]: https://semver.org -package semver diff --git a/internal/semver/semver.go b/internal/semver/semver.go deleted file mode 100644 index 89a3c68..0000000 --- a/internal/semver/semver.go +++ /dev/null @@ -1,72 +0,0 @@ -package semver - -import ( - "fmt" - "regexp" - "strings" - - iprint "github.com/opensourcecorp/oscar/internal/print" - xsemver "golang.org/x/mod/semver" -) - -// Get tries to build a compliant Semantic Version number out of the provided string, regardless of -// how dirty it is. Despite using the "golang.org/x/mod/semver" package in a few places internally, -// most of this implementation is custom due to limitations in that package -- like not being able -// to parse out just the Patch number, or Pre-Release/Build numbers not being allowed in -// semver.Canonical() -func Get(s string) (string, error) { - // Grab the semver parts separately so we can clean them up. Firstly, the Major-Minor-Patch - // parts, but Patch takes some extra work to suss out -- if there's any prerelease or build - // parts of the version, these show up in the "patch" index if we just split on dots, so we can - // just grab the whole MMP with a regex - matchList := regexp.MustCompile(`[0-9]+(\.[0-9]+)?(\.[0-9]+)?`).FindStringSubmatch(s) - if len(matchList) == 0 { - return "", fmt.Errorf("malformed or unmatchable Semantic Version number (got: '%s')", s) - } - v := matchList[0] - - // NOTE: the external semver package has some niceties, and we use them here, but since it's a - // Go package it expects a "v" prefix on every number. We want to just keep the non-"v" data - // since it's more portable, so we need to self-prefix the version number for the remaining - // duration of this function. - v = xsemver.Canonical("v" + v) - if v == "" { - return "", fmt.Errorf("unable to canonicalize provided version '%s' (after possibly converting to '%s')", s, v) - } - - // Gross. - var preRelease, build string - prSplit := strings.Split(s, "-") - bSplit := strings.Split(s, "+") - if len(prSplit) > 1 { - // could still have a build number - preRelease = strings.Split(prSplit[1], "+")[0] - } - if len(bSplit) > 1 { - build = bSplit[1] - } - - if preRelease != "" { - v += "-" + preRelease - } - if build != "" { - v += "+" + build - } - - if !xsemver.IsValid(v) { - return "", fmt.Errorf("could not understand the Semantic Version you provided (got: '%s', converted to: '%s')", s, v) - } - - // NOW, we can finally strip off the "v" prefix - v = strings.TrimPrefix(v, "v") - - return v, nil -} - -// VersionWasIncremented reports whether the newVersion is greater than the oldVersion. -func VersionWasIncremented(newVersion string, oldVersion string) bool { - compValue := xsemver.Compare("v"+newVersion, "v"+oldVersion) - iprint.Debugf("semver comparison value: %d\n", compValue) - - return compValue > 0 -} diff --git a/internal/semver/semver_test.go b/internal/semver/semver_test.go deleted file mode 100644 index 4a957be..0000000 --- a/internal/semver/semver_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package semver - -import ( - "testing" -) - -func TestGetSemver(t *testing.T) { - t.Run("No conversion on a basic conformant semver", func(t *testing.T) { - s := "1.0.0" - want := "1.0.0" - got, err := Get(s) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if want != got { - t.Errorf("Expected version string '%s' to become '%s', but got '%s'\n", s, want, got) - } - }) - - t.Run("No conversion on a full conformant semver", func(t *testing.T) { - s := "1.1.9-prebeta1+abc" - want := s - got, err := Get(s) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if want != got { - t.Errorf("Expected version string '%s' to become '%s', but got '%s'\n", s, want, got) - } - }) - - t.Run("Convert semver with prerelease info to be conformant", func(t *testing.T) { - s := "1.0-alpha" - want := "1.0.0-alpha" - got, err := Get(s) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if want != got { - t.Errorf("Expected version string '%s' to become '%s', but got '%s'\n", s, want, got) - } - }) - - t.Run("Convert semver with build info to be conformant", func(t *testing.T) { - s := "1.0+abc" - want := "1.0.0+abc" - got, err := Get(s) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if want != got { - t.Errorf("Expected version string '%s' to become '%s', but got '%s'\n", s, want, got) - } - }) -} diff --git a/internal/tasks/ci/run.go b/internal/tasks/ci/run.go index c9cbd24..32a0d96 100644 --- a/internal/tasks/ci/run.go +++ b/internal/tasks/ci/run.go @@ -8,7 +8,7 @@ import ( "time" "github.com/opensourcecorp/oscar/internal/consts" - "github.com/opensourcecorp/oscar/internal/git" + igit "github.com/opensourcecorp/oscar/internal/git" iprint "github.com/opensourcecorp/oscar/internal/print" containertools "github.com/opensourcecorp/oscar/internal/tasks/tools/containers" gotools "github.com/opensourcecorp/oscar/internal/tasks/tools/go" @@ -70,7 +70,7 @@ func Run(ctx context.Context) (err error) { } // For tracking any changes to Git status etc. after each CI Task runs - gitCI, err := git.NewForCI(ctx) + git, err := igit.NewForCI(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } @@ -89,10 +89,10 @@ func Run(ctx context.Context) (err error) { runErr = errors.Join(runErr, task.Exec(ctx)) runErr = errors.Join(runErr, task.Post(ctx)) - if err := gitCI.Update(ctx); err != nil { + if err := git.Update(ctx); err != nil { return fmt.Errorf("internal error: %w", err) } - gitStatusHasChanged, err := gitCI.StatusHasChanged(ctx) + gitStatusHasChanged, err := git.StatusHasChanged(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } @@ -106,15 +106,15 @@ func Run(ctx context.Context) (err error) { } if gitStatusHasChanged { - iprint.Errorf("Files ~CHANGED~ during run: %+v\n", gitCI.CurrentStatus.Diff) - iprint.Errorf("Files +CREATED+ during run: %+v\n", gitCI.CurrentStatus.UntrackedFiles) + iprint.Errorf("Files ~CHANGED~ during run: %+v\n", git.CurrentStatus.Diff) + iprint.Errorf("Files +CREATED+ during run: %+v\n", git.CurrentStatus.UntrackedFiles) iprint.Errorf("\n") } run.Failures = append(run.Failures, fmt.Sprintf("%s :: %s", lang, task.InfoText())) // Also need to reset the baseline status - gitCI, err = git.NewForCI(ctx) + git, err = igit.NewForCI(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } diff --git a/internal/tasks/tools/containers/deliver.go b/internal/tasks/tools/containers/deliver.go index b38a6ed..7ebc038 100644 --- a/internal/tasks/tools/containers/deliver.go +++ b/internal/tasks/tools/containers/deliver.go @@ -7,6 +7,8 @@ import ( "path/filepath" "strings" + oscarcfgpbv1 "github.com/opensourcecorp/oscar/internal/generated/opensourcecorp/oscar/config/v1" + igit "github.com/opensourcecorp/oscar/internal/git" "github.com/opensourcecorp/oscar/internal/oscarcfg" iprint "github.com/opensourcecorp/oscar/internal/print" taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" @@ -19,14 +21,16 @@ type ( type registryMapping struct { Name string - GitHub struct { - AuthCommand []string - } + GitHub gitHubRegistry +} + +type gitHubRegistry struct { + AuthCommand []string } func newRegistryMap(username string) registryMapping { return registryMapping{ - GitHub: struct{ AuthCommand []string }{ + GitHub: gitHubRegistry{ AuthCommand: []string{"bash", "-c", fmt.Sprintf(` echo ${GITHUB_TOKEN} | docker login ghcr.io --username %s --password-stdin `, username, @@ -42,10 +46,10 @@ func NewTasksForDelivery(repo taskutil.Repo) ([]taskutil.Tasker, error) { return nil, err } - if repo.HasContainerfile && cfg.Deliver != nil { + if repo.HasContainerfile { out := make([]taskutil.Tasker, 0) - if cfg.Deliver.ContainerImage != nil { + if cfg.GetDeliverables().GetContainerImage() != nil { out = append(out, imageBuildPush{}) } @@ -64,7 +68,12 @@ func (t imageBuildPush) Exec(ctx context.Context) error { if err != nil { return err } - cfg := rootCfg.Deliver.ContainerImage + cfg := rootCfg.GetDeliverables().GetContainerImage() + + uri, err := constructImageURI(ctx, rootCfg) + if err != nil { + return fmt.Errorf("constructing image URI: %w", err) + } composeFileContents, err := os.ReadFile("docker-compose.yaml") if err != nil { @@ -77,16 +86,6 @@ func (t imageBuildPush) Exec(ctx context.Context) error { } iprint.Debugf("composeFile unmarshalled: %#v\n", composeFile) - // TODO: use a validator package instead, so we can check all the fields more easily - if cfg.Repo == "" { - return fmt.Errorf("required 'repo' key not set for this Deliverable in oscar.yaml") - } - - uri := fmt.Sprintf( - "%s/%s/%s:%s", - cfg.Registry, cfg.Owner, cfg.Repo, rootCfg.Version, - ) - curDir, err := os.Getwd() if err != nil { return err @@ -135,3 +134,27 @@ func (t imageBuildPush) Exec(ctx context.Context) error { // Post implements [taskutil.Tasker.Post]. func (t imageBuildPush) Post(_ context.Context) error { return nil } + +// TODO +func constructImageURI(ctx context.Context, rootCfg *oscarcfgpbv1.Config) (string, error) { + cfg := rootCfg.GetDeliverables().GetContainerImage() + git, err := igit.New(ctx) + if err != nil { + return "", fmt.Errorf("getting Git info: %w", err) + } + + tag := rootCfg.GetVersion() + if git.Branch != "main" { + tag = fmt.Sprintf("%s-%s", git.SanitizedBranch(), git.LatestCommit) + } + if git.IsDirty { + tag = fmt.Sprintf("%s-%s-dirty", git.SanitizedBranch(), git.LatestCommit) + } + + uri := fmt.Sprintf( + "%s/%s/%s:%s", + cfg.GetRegistry(), cfg.GetOwner(), cfg.GetRepo(), tag, + ) + + return uri, nil +} diff --git a/internal/tasks/tools/containers/deliver_test.go b/internal/tasks/tools/containers/deliver_test.go new file mode 100644 index 0000000..30a7a55 --- /dev/null +++ b/internal/tasks/tools/containers/deliver_test.go @@ -0,0 +1,32 @@ +package containertools + +import ( + "context" + "regexp" + "testing" + + igit "github.com/opensourcecorp/oscar/internal/git" + "github.com/opensourcecorp/oscar/internal/oscarcfg" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConstructImageURI(t *testing.T) { + cfg, err := oscarcfg.Get("../../../oscarcfg/test.oscar.yaml") + require.NoError(t, err) + + git, err := igit.New(context.Background()) + require.NoError(t, err) + + var want *regexp.Regexp + if git.Branch == "main" { + want = regexp.MustCompile(`ghcr.io/opensourcecorp/oscar:` + cfg.GetVersion()) + } else { + want = regexp.MustCompile(`ghcr.io/opensourcecorp/oscar:.*-.*-.*`) + } + + got, err := constructImageURI(context.Background(), cfg) + require.NoError(t, err) + + assert.Regexp(t, want, got) +} diff --git a/internal/tasks/tools/go/deliver.go b/internal/tasks/tools/go/deliver.go index 8fa526d..a3c2983 100644 --- a/internal/tasks/tools/go/deliver.go +++ b/internal/tasks/tools/go/deliver.go @@ -22,10 +22,10 @@ func NewTasksForDelivery(repo taskutil.Repo) ([]taskutil.Tasker, error) { return nil, err } - if repo.HasGo && cfg.Deliver != nil { + if repo.HasGo { out := make([]taskutil.Tasker, 0) - if cfg.Deliver.GoGitHubRelease != nil { + if cfg.GetDeliverables().GetGoGithubRelease() != nil { out = append(out, ghRelease{}) } @@ -46,7 +46,7 @@ func (t ghRelease) Exec(ctx context.Context) error { } var buildErr error - for _, src := range cfg.Deliver.GoGitHubRelease.BuildSources { + for _, src := range cfg.GetDeliverables().GetGoGithubRelease().GetBuildSources() { buildErr = goBuild(ctx, src) } if buildErr != nil { diff --git a/internal/tasks/tools/toolcfg/.yamlfmt b/internal/tasks/tools/toolcfg/.yamlfmt index 6f70c49..e350023 100644 --- a/internal/tasks/tools/toolcfg/.yamlfmt +++ b/internal/tasks/tools/toolcfg/.yamlfmt @@ -8,4 +8,5 @@ formatter: indent: 2 line_ending: "lf" max_line_length: 100 + retain_line_breaks_single: true trim_trailing_whitespace: true diff --git a/internal/tasks/tools/toolcfg/.yamllint b/internal/tasks/tools/toolcfg/.yamllint index 14d62ac..f487d55 100644 --- a/internal/tasks/tools/toolcfg/.yamllint +++ b/internal/tasks/tools/toolcfg/.yamllint @@ -6,6 +6,8 @@ rules: require-starting-space: true ignore-shebangs: true min-spaces-from-content: 1 + level: "error" + comments-indentation: "disable" empty-lines: max: 1 max-start: 0 @@ -20,3 +22,4 @@ rules: level: "error" truthy: check-keys: false # mostly to allow GitHub Actions' "on" key to pass + level: "error" diff --git a/internal/tasks/tools/toolcfg/staticcheck.conf b/internal/tasks/tools/toolcfg/staticcheck.conf index 098bfc6..2edb311 100644 --- a/internal/tasks/tools/toolcfg/staticcheck.conf +++ b/internal/tasks/tools/toolcfg/staticcheck.conf @@ -2,4 +2,6 @@ # https://staticcheck.dev/docs/checks/ checks = [ "all", + # staticcheck won't ignore generated code for this one, so revive handles this check instead + "-ST1000" ] diff --git a/internal/tasks/tools/version/ci.go b/internal/tasks/tools/version/ci.go index d064af6..22765f5 100644 --- a/internal/tasks/tools/version/ci.go +++ b/internal/tasks/tools/version/ci.go @@ -11,7 +11,6 @@ import ( "github.com/opensourcecorp/oscar/internal/consts" "github.com/opensourcecorp/oscar/internal/oscarcfg" iprint "github.com/opensourcecorp/oscar/internal/print" - "github.com/opensourcecorp/oscar/internal/semver" taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) @@ -33,7 +32,7 @@ func (t versionCI) Exec(ctx context.Context) (err error) { if err != nil { return fmt.Errorf("getting oscar config: %w", err) } - version := cfg.Version + version := cfg.GetVersion() iprint.Debugf("provided version: %s\n", version) // NOTE: we clone the repo in question to a temp location to check the version on @@ -65,7 +64,7 @@ func (t versionCI) Exec(ctx context.Context) (err error) { if err != nil { return fmt.Errorf("getting oscar config: %w", err) } - mainVersion := mainCfg.Version + mainVersion := mainCfg.GetVersion() iprint.Debugf("main version: %s\n", version) // Need to check if we're already on the main branch, since checking its version against itself @@ -80,7 +79,7 @@ func (t versionCI) Exec(ctx context.Context) (err error) { iprint.Debugf("current Git branch/ref: %s\n", branch) if branch != "main" { - if !semver.VersionWasIncremented(version, mainVersion) { + if !oscarcfg.VersionHasBeenIncremented(version, mainVersion) { return fmt.Errorf( "version in oscar config on this branch (%s) has not been incremented from the version on the main branch", version, diff --git a/internal/tasks/tools/version/ci_test.go b/internal/tasks/tools/version/ci_test.go index 536cd0e..e8ab971 100644 --- a/internal/tasks/tools/version/ci_test.go +++ b/internal/tasks/tools/version/ci_test.go @@ -1,13 +1,14 @@ package versiontools -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestCanonicalizeGitRemote(t *testing.T) { remote := "git@github.com:opensourcecorp/oscar.git" want := "https://github.com/opensourcecorp/oscar.git" got := canonicalizeGitRemote(remote) - - if want != got { - t.Errorf("\nwant: %v\ngot: %v", want, got) - } + assert.Equal(t, want, got) } diff --git a/mise.toml b/mise.toml index c6139a1..6331805 100644 --- a/mise.toml +++ b/mise.toml @@ -3,27 +3,31 @@ experimental = true jobs = 4 [tools] -github-cli = "prefix:2" -go = "prefix:1" -hadolint = "prefix:2" -markdownlint-cli2 = "prefix:0.18" +buf = "1.57.2" +github-cli = "2.79.0" +go = "1.25.1" +hadolint = "2.13.1" +markdownlint-cli2 = "0.18.1" # required for markdownlint-cli2 -node = "prefix:24" -ripgrep = "prefix:14" -shellcheck = "prefix:0.11" -shfmt = "prefix:3" -terraform = "prefix:1.13" -uv = "prefix:0.8" -yamlfmt = "prefix:0.17" -yamllint = "prefix:1" +node = "24.8.0" +protobuf = "32.1" +ripgrep = "14.1.1" +shellcheck = "0.11.0" +shfmt = "3.12.0" +terraform = "1.13.3" +uv = "0.8.18" +yamlfmt = "0.17.2" +yamllint = "1.37.1" -# Go-specific tools -"go:github.com/kisielk/errcheck" = { version = "prefix:v1" } -"go:github.com/mgechev/revive" = { version = "prefix:v1" } -"go:golang.org/x/tools/cmd/goimports" = { version = "latest" } -"go:golang.org/x/vuln/cmd/govulncheck" = { version = "prefix:v1" } -"go:honnef.co/go/tools/cmd/staticcheck" = { version = "latest" } +### Go-specific tools, for oscar or its development +"go:github.com/kisielk/errcheck" = { version = "1.9.0" } +"go:github.com/mgechev/revive" = { version = "1.12.0" } +"go:golang.org/x/tools/cmd/goimports" = { version = "0.37.0" } +"go:golang.org/x/vuln/cmd/govulncheck" = { version = "1.1.4" } +"go:google.golang.org/protobuf/cmd/protoc-gen-go" = { version = "1.36.8" } +# gets mad about Go toolchain versions if not "latest" +"go:honnef.co/go/tools/cmd/staticcheck" = { version = "2025.1.1" } -# Python-specific tools. Note that others that may also be CLI tools, may be run internally via -# `uvx` instead. -ruff = "prefix:0.13" +### Python-specific tools. Note that others that may also be CLI tools, may be run internally via +### `uvx` instead. +ruff = "0.13.1" diff --git a/oscar.yaml b/oscar.yaml index fe48ddd..2f0dbb0 100644 --- a/oscar.yaml +++ b/oscar.yaml @@ -1,8 +1,7 @@ --- version: "0.2.0" -deliver: +deliverables: go_github_release: - repo: "opensourcecorp/oscar" build_sources: - "./cmd/oscar" draft: false diff --git a/proto/buf.gen.yaml b/proto/buf.gen.yaml new file mode 100644 index 0000000..e257007 --- /dev/null +++ b/proto/buf.gen.yaml @@ -0,0 +1,12 @@ +--- +version: "v2" +clean: true +plugins: + # google.golang.org/protobuf/cmd/protoc-gen-go + - local: "protoc-gen-go" + out: "../internal/generated" + opt: + - "paths=source_relative" + # # ../proto/generate.go + # - local: "../build/protoc-gen-oscarcfg" + # out: "../internal/generated" diff --git a/proto/buf.lock b/proto/buf.lock new file mode 100644 index 0000000..bb66131 --- /dev/null +++ b/proto/buf.lock @@ -0,0 +1,6 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/bufbuild/protovalidate + commit: 52f32327d4b045a79293a6ad4e7e1236 + digest: b5:cbabc98d4b7b7b0447c9b15f68eeb8a7a44ef8516cb386ac5f66e7fd4062cd6723ed3f452ad8c384b851f79e33d26e7f8a94e2b807282b3def1cd966c7eace97 diff --git a/proto/buf.yaml b/proto/buf.yaml new file mode 100644 index 0000000..7c0c5ea --- /dev/null +++ b/proto/buf.yaml @@ -0,0 +1,18 @@ +--- +version: "v2" +name: "github.com/opensourcecorp/oscar" +deps: + - "buf.build/bufbuild/protovalidate" +lint: + use: + - "STANDARD" + - "COMMENTS" + except: + - "ENUM_VALUE_PREFIX" + - "SERVICE_SUFFIX" + - "RPC_REQUEST_STANDARD_NAME" + - "RPC_RESPONSE_STANDARD_NAME" + - "RPC_REQUEST_RESPONSE_UNIQUE" +breaking: + use: + - "FILE" diff --git a/proto/generate.go b/proto/generate.go new file mode 100644 index 0000000..71d677f --- /dev/null +++ b/proto/generate.go @@ -0,0 +1,11 @@ +// Package main +package main + +import "fmt" + +//go:generate go build -o ../build/protoc-gen-oscarcfg . +//go:generate buf generate + +func main() { + fmt.Println("not implemented, but this generator should generate a populated oscar.yaml") +} diff --git a/proto/opensourcecorp/oscar/config/v1/config.proto b/proto/opensourcecorp/oscar/config/v1/config.proto new file mode 100644 index 0000000..bccfe80 --- /dev/null +++ b/proto/opensourcecorp/oscar/config/v1/config.proto @@ -0,0 +1,55 @@ +syntax = "proto3"; + +package opensourcecorp.oscar.config.v1; + +import "buf/validate/validate.proto"; + +option go_package = "github.com/opensourcecorp/oscar/internal/generated/opensourcecorp/oscar/config/v1;oscarcfgpbv1"; + +// Config defines the top-level structure of oscar's config file. +message Config { + // Version is the version string for the codebase. Must be a valid Semantic Version string. + // + // Example: "1.0.0" + string version = 1 [(buf.validate.field).string.pattern = "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9]+)?(\\+[a-zA-Z0-9]+)?$"]; + // Deliverables is the collection of possible deliverable artifacts. + Deliverables deliverables = 2; +} + +// Deliverables contains a field for each possible deliverable. +message Deliverables { + // See [GoGitHubRelease]. + GoGitHubRelease go_github_release = 1; + // See [ContainerImage]. + ContainerImage container_image = 2; +} + +// GoGitHubRelease defines the arguments necessary to create GitHub Releases for Go binaries. +message GoGitHubRelease { + // The filepaths to the "main" packages to be built. + // + // Example: - "./cmd/oscar" + repeated string build_sources = 1 [(buf.validate.field).required = true]; + // Optionally flags whether the Release should be left in Draft state at create-time. This can + // be useful to set if you want to review the Release contents before actually publishing. + // + // Example: false + bool draft = 2; +} + +// ContainerImage defines the arguments necessary to build & push container image artifacts. +message ContainerImage { + // The target registry provider domain. + // + // Example: "ghcr.io" + string registry = 1 [(buf.validate.field).required = true]; + // The target OCI repository name. + // + // Example: "opensourcecorp" + string owner = 2 [(buf.validate.field).required = true]; + // The target OCI repository. May contain as many subpaths to the actual image artifact as + // necessary based on the registry, e.g. "my-repo/my-image-group/my-image". + // + // Example: "oscar" + string repo = 3 [(buf.validate.field).required = true]; +} From ef78f555088734e7936f6739837c6fa0477d03fc Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 00:23:01 -0500 Subject: [PATCH 18/38] Also have goGenerate run formatters to prevent Git diffs --- internal/git/git.go | 4 +++ internal/tasks/tools/go/ci.go | 56 ++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/internal/git/git.go b/internal/git/git.go index e30c3d1..6aa2038 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -10,6 +10,7 @@ import ( taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) +// Git holds metadata about the current state of the Git repository. type Git struct { Root string Branch string @@ -24,6 +25,7 @@ type Status struct { UntrackedFiles []string } +// New returns a populated [Git]. func New(ctx context.Context) (*Git, error) { root, err := taskutil.RunCommand(ctx, []string{"git", "rev-parse", "--show-toplevel"}) if err != nil { @@ -71,6 +73,8 @@ func New(ctx context.Context) (*Git, error) { return &out, nil } +// SanitizedBranch returns the current branch name, sanitized for various systems that allow for a +// smaller charset (e.g. container image tags). func (g *Git) SanitizedBranch() string { return regexp.MustCompile(`[_/]`).ReplaceAllString(g.Branch, "-") } diff --git a/internal/tasks/tools/go/ci.go b/internal/tasks/tools/go/ci.go index fd844a5..7968673 100644 --- a/internal/tasks/tools/go/ci.go +++ b/internal/tasks/tools/go/ci.go @@ -38,6 +38,11 @@ func NewTasksForCI(repo taskutil.Repo) []taskutil.Tasker { RunArgs: []string{"go", "fmt", "./..."}, }, }, + goImports{ + Tool: taskutil.Tool{ + RunArgs: []string{"goimports", "-l", "-w", "."}, + }, + }, generateCodeCI{ Tool: taskutil.Tool{ RunArgs: []string{"go", "generate", "./..."}, @@ -74,11 +79,6 @@ func NewTasksForCI(repo taskutil.Repo) []taskutil.Tasker { RunArgs: []string{"errcheck", "./..."}, }, }, - goImports{ - Tool: taskutil.Tool{ - RunArgs: []string{"goimports", "-l", "-w", "."}, - }, - }, govulncheck{ Tool: taskutil.Tool{ RunArgs: []string{"govulncheck", "./..."}, @@ -125,6 +125,21 @@ func (t goFormat) Exec(ctx context.Context) error { // Post implements [taskutil.Tasker.Post]. func (t goFormat) Post(_ context.Context) error { return nil } +// InfoText implements [taskutil.Tasker.InfoText]. +func (t goImports) InfoText() string { return "Format imports" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t goImports) Exec(ctx context.Context) error { + if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t goImports) Post(_ context.Context) error { return nil } + // InfoText implements [taskutil.Tasker.InfoText]. func (t generateCodeCI) InfoText() string { return "Generate code" } @@ -134,6 +149,22 @@ func (t generateCodeCI) Exec(ctx context.Context) error { return err } + // Generating code will likely throw diffs if not also addressing other formatting CI checks, so + // run those here again as well. + tasks := NewTasksForCI(taskutil.Repo{HasGo: true}) + for _, task := range tasks { + if wantToRun, isOfType := task.(goFormat); isOfType { + if err := wantToRun.Exec(ctx); err != nil { + return fmt.Errorf("running Go formatter after code generation: %w", err) + } + } + if wantToRun, isOfType := task.(goImports); isOfType { + if err := wantToRun.Exec(ctx); err != nil { + return fmt.Errorf("running Go formatter after code generation: %w", err) + } + } + } + return nil } @@ -235,21 +266,6 @@ func (t errcheck) Exec(ctx context.Context) error { // Post implements [taskutil.Tasker.Post]. func (t errcheck) Post(_ context.Context) error { return nil } -// InfoText implements [taskutil.Tasker.InfoText]. -func (t goImports) InfoText() string { return "Format imports" } - -// Exec implements [taskutil.Tasker.Exec]. -func (t goImports) Exec(ctx context.Context) error { - if _, err := taskutil.RunCommand(ctx, t.RunArgs); err != nil { - return err - } - - return nil -} - -// Post implements [taskutil.Tasker.Post]. -func (t goImports) Post(_ context.Context) error { return nil } - // InfoText implements [taskutil.Tasker.InfoText]. func (t govulncheck) InfoText() string { return "Vulnerability scan (govulncheck)" } From 8f03ec755066a45dffb330001e48843c7fb2f2cf Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 01:06:37 -0500 Subject: [PATCH 19/38] Print Git info at the start of runs --- internal/git/git.go | 18 ++++++++++++++++++ internal/tasks/ci/run.go | 18 ++++++++++++------ internal/tasks/delivery/run.go | 7 +++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/internal/git/git.go b/internal/git/git.go index 6aa2038..98ff2dd 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -3,6 +3,7 @@ package igit import ( "context" "fmt" + "reflect" "regexp" "strings" @@ -79,6 +80,23 @@ func (g *Git) SanitizedBranch() string { return regexp.MustCompile(`[_/]`).ReplaceAllString(g.Branch, "-") } +// String implements [fmt.Stringer]. +func (g *Git) String() string { + out := "Current Git information:\n" + + t := reflect.TypeOf(*g) + v := reflect.ValueOf(*g) + for i := range v.NumField() { + field := t.Field(i) + value := v.Field(i) + out += fmt.Sprintf("- %s: %v\n", field.Name, value) + } + + out += "\n" + + return out +} + // getRawStatus returns a slightly-modified "git status" output, so that calling tools can parse it // more easily. func getRawStatus(ctx context.Context) (Status, error) { diff --git a/internal/tasks/ci/run.go b/internal/tasks/ci/run.go index 32a0d96..fa66e30 100644 --- a/internal/tasks/ci/run.go +++ b/internal/tasks/ci/run.go @@ -58,6 +58,12 @@ func Run(ctx context.Context) (err error) { return fmt.Errorf("internal error setting up run info: %w", err) } + git, err := igit.New(ctx) + if err != nil { + return err + } + fmt.Print(git.String()) + repo, err := taskutil.NewRepo(ctx) if err != nil { return fmt.Errorf("getting repo composition: %w", err) @@ -70,7 +76,7 @@ func Run(ctx context.Context) (err error) { } // For tracking any changes to Git status etc. after each CI Task runs - git, err := igit.NewForCI(ctx) + gitCI, err := igit.NewForCI(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } @@ -89,10 +95,10 @@ func Run(ctx context.Context) (err error) { runErr = errors.Join(runErr, task.Exec(ctx)) runErr = errors.Join(runErr, task.Post(ctx)) - if err := git.Update(ctx); err != nil { + if err := gitCI.Update(ctx); err != nil { return fmt.Errorf("internal error: %w", err) } - gitStatusHasChanged, err := git.StatusHasChanged(ctx) + gitStatusHasChanged, err := gitCI.StatusHasChanged(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } @@ -106,15 +112,15 @@ func Run(ctx context.Context) (err error) { } if gitStatusHasChanged { - iprint.Errorf("Files ~CHANGED~ during run: %+v\n", git.CurrentStatus.Diff) - iprint.Errorf("Files +CREATED+ during run: %+v\n", git.CurrentStatus.UntrackedFiles) + iprint.Errorf("Files ~CHANGED~ during run: %+v\n", gitCI.CurrentStatus.Diff) + iprint.Errorf("Files +CREATED+ during run: %+v\n", gitCI.CurrentStatus.UntrackedFiles) iprint.Errorf("\n") } run.Failures = append(run.Failures, fmt.Sprintf("%s :: %s", lang, task.InfoText())) // Also need to reset the baseline status - git, err = igit.NewForCI(ctx) + gitCI, err = igit.NewForCI(ctx) if err != nil { return fmt.Errorf("internal error: %w", err) } diff --git a/internal/tasks/delivery/run.go b/internal/tasks/delivery/run.go index 1b4b25c..84b8605 100644 --- a/internal/tasks/delivery/run.go +++ b/internal/tasks/delivery/run.go @@ -8,6 +8,7 @@ import ( "time" "github.com/opensourcecorp/oscar/internal/consts" + igit "github.com/opensourcecorp/oscar/internal/git" iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/tasks/ci" containertools "github.com/opensourcecorp/oscar/internal/tasks/tools/containers" @@ -60,6 +61,12 @@ func Run(ctx context.Context) (err error) { return fmt.Errorf("internal error setting up run info: %w", err) } + git, err := igit.New(ctx) + if err != nil { + return err + } + fmt.Print(git.String()) + repo, err := taskutil.NewRepo(ctx) if err != nil { return fmt.Errorf("getting repo composition: %w", err) From fb4535c19cc745ffb8e4a653af2e8799acd1f09e Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 01:15:01 -0500 Subject: [PATCH 20/38] oh great heavens I was using a global Python --- mise.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/mise.toml b/mise.toml index 6331805..ef82227 100644 --- a/mise.toml +++ b/mise.toml @@ -11,6 +11,7 @@ markdownlint-cli2 = "0.18.1" # required for markdownlint-cli2 node = "24.8.0" protobuf = "32.1" +python = "3.13.7" ripgrep = "14.1.1" shellcheck = "0.11.0" shfmt = "3.12.0" From 8a22debfa3ae0fc12865140247a97090ec528104 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 01:20:30 -0500 Subject: [PATCH 21/38] omfg gha --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1302294..86e7931 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,7 +12,7 @@ on: - "synchronize" env: - GITHUB_TOKEN: "{{ secrets.GITHUB_TOKEN }}" + GITHUB_TOKEN: "{{ github.token }}" jobs: ci: From b31b437c0eeeabdbf07301c1fb3feb1a5d717833 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 01:21:31 -0500 Subject: [PATCH 22/38] aint no way this works --- .github/workflows/main.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 86e7931..ce65e3c 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -11,8 +11,8 @@ on: - "reopened" - "synchronize" -env: - GITHUB_TOKEN: "{{ github.token }}" +# env: +# GITHUB_TOKEN: "{{ github.token }}" jobs: ci: From 93295c009c52e8e6da302fa75354bef2b6c0afad Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 01:25:26 -0500 Subject: [PATCH 23/38] Unreal but ok --- .github/workflows/main.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index ce65e3c..e7f753c 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -11,9 +11,6 @@ on: - "reopened" - "synchronize" -# env: -# GITHUB_TOKEN: "{{ github.token }}" - jobs: ci: runs-on: "ubuntu-latest" @@ -56,6 +53,8 @@ jobs: # cache: true # - name: "Run Delivery Tasks" # run: "make deliver" + # env: + # GITHUB_TOKEN: "{{ secrets.GITHUB_TOKEN }}" # - name: Set up Docker Buildx # uses: docker/setup-buildx-action@v2 From 887235ed831343fedac20a4276c16efe46838c40 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 11:57:38 -0500 Subject: [PATCH 24/38] Test a tweak because I'm an idiot --- .github/workflows/main.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index e7f753c..2afb7b4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -11,6 +11,11 @@ on: - "reopened" - "synchronize" +env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + MISE_GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + jobs: ci: runs-on: "ubuntu-latest" @@ -54,7 +59,7 @@ jobs: # - name: "Run Delivery Tasks" # run: "make deliver" # env: - # GITHUB_TOKEN: "{{ secrets.GITHUB_TOKEN }}" + # GITHUB_TOKEN: "${{ github.token }}" # - name: Set up Docker Buildx # uses: docker/setup-buildx-action@v2 @@ -63,7 +68,7 @@ jobs: # with: # registry: ghcr.io # username: ${{ github.repository_owner }} - # password: ${{ secrets.GITHUB_TOKEN }} + # password: ${{ github.token }} # # Generate any tags we want for the images # # (https://github.com/docker/build-push-action/blob/master/docs/advanced/tags-labels.md) From 1e6da61741bf5a25476bd137ea6fd4d847ffaaf0 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 12:46:56 -0500 Subject: [PATCH 25/38] Maybe some finishing work before moving to GHA changes --- .gitignore | 12 +++---- Containerfile | 2 +- internal/tasks/delivery/run.go | 7 ++-- internal/tasks/tools/gittag/deliver.go | 48 ++++++++++++++++++++++++++ internal/tasks/tools/gittag/doc.go | 2 ++ internal/tasks/tools/go/deliver.go | 29 +++++++++++++++- 6 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 internal/tasks/tools/gittag/deliver.go create mode 100644 internal/tasks/tools/gittag/doc.go diff --git a/.gitignore b/.gitignore index e882d75..82b0a5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,11 @@ +.oscar/ .vscode/ -*cache* *.log - *.out -build/ -dist/ *.tar.gz *.zip - -.oscar/ - +*cache* +build/ +dist/ +mise.*.toml scratch/ diff --git a/Containerfile b/Containerfile index 4a62c85..7eb599d 100644 --- a/Containerfile +++ b/Containerfile @@ -58,7 +58,7 @@ COPY --from=builder /go/app/build/oscar /oscar # NOTE: Docker BuildKit will skip stages it doesn't see as dependencies, so to enforce the "ci" # stage above to run, we need to force a dependency here -COPY --from=ci /go/app/oscar.yaml /oscar.yaml +COPY --from=ci /go/app/LICENSE /LICENSE RUN apt-get update && apt-get install --no-install-recommends -y \ bash \ diff --git a/internal/tasks/delivery/run.go b/internal/tasks/delivery/run.go index 84b8605..d4fe11a 100644 --- a/internal/tasks/delivery/run.go +++ b/internal/tasks/delivery/run.go @@ -12,6 +12,7 @@ import ( iprint "github.com/opensourcecorp/oscar/internal/print" "github.com/opensourcecorp/oscar/internal/tasks/ci" containertools "github.com/opensourcecorp/oscar/internal/tasks/tools/containers" + gittagtools "github.com/opensourcecorp/oscar/internal/tasks/tools/gittag" gotools "github.com/opensourcecorp/oscar/internal/tasks/tools/go" taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" ) @@ -21,8 +22,10 @@ import ( func getDeliveryTaskMap(repo taskutil.Repo) (taskutil.TaskMap, error) { out := make(taskutil.TaskMap) for langName, getTasksFunc := range map[string]func(taskutil.Repo) ([]taskutil.Tasker, error){ - "Go": gotools.NewTasksForDelivery, - "OCI Images": containertools.NewTasksForDelivery, + // Independent of Delivery tasks, always push a Git Tag first + "0 - Create Git Tag": gittagtools.NewTasksForDelivery, + "Go": gotools.NewTasksForDelivery, + "OCI Images": containertools.NewTasksForDelivery, // "Python": pytools.NewTasksForDelivery, // "Terraform": tftools.NewTasksForDelivery, // "Markdown": mdtools.NewTasksForDelivery, diff --git a/internal/tasks/tools/gittag/deliver.go b/internal/tasks/tools/gittag/deliver.go new file mode 100644 index 0000000..54cfb99 --- /dev/null +++ b/internal/tasks/tools/gittag/deliver.go @@ -0,0 +1,48 @@ +package gittagtools + +import ( + "context" + "fmt" + + "github.com/opensourcecorp/oscar/internal/oscarcfg" + taskutil "github.com/opensourcecorp/oscar/internal/tasks/util" +) + +type ( + createAndPushTag struct{ taskutil.Tool } +) + +// NewTasksForDelivery returns the list of Delivery tasks. +func NewTasksForDelivery(_ taskutil.Repo) ([]taskutil.Tasker, error) { + out := []taskutil.Tasker{ + createAndPushTag{}, + } + + return out, nil +} + +// InfoText implements [taskutil.Tasker.InfoText]. +func (t createAndPushTag) InfoText() string { return "Create & Push Git Tag" } + +// Exec implements [taskutil.Tasker.Exec]. +func (t createAndPushTag) Exec(ctx context.Context) error { + cfg, err := oscarcfg.Get() + if err != nil { + return err + } + + args := []string{"bash", "-c", fmt.Sprintf(` + git tag v%s + git push --tags + `, cfg.GetVersion(), + )} + + if _, err := taskutil.RunCommand(ctx, args); err != nil { + return err + } + + return nil +} + +// Post implements [taskutil.Tasker.Post]. +func (t createAndPushTag) Post(_ context.Context) error { return nil } diff --git a/internal/tasks/tools/gittag/doc.go b/internal/tasks/tools/gittag/doc.go new file mode 100644 index 0000000..b34ae7a --- /dev/null +++ b/internal/tasks/tools/gittag/doc.go @@ -0,0 +1,2 @@ +// Package gittagtools contains logic for running tasks for Git Tags. +package gittagtools diff --git a/internal/tasks/tools/go/deliver.go b/internal/tasks/tools/go/deliver.go index a3c2983..e004d2f 100644 --- a/internal/tasks/tools/go/deliver.go +++ b/internal/tasks/tools/go/deliver.go @@ -53,7 +53,34 @@ func (t ghRelease) Exec(ctx context.Context) error { return err } - // TODO: actually write GH Release code + buildDir := "build" + distDir := "dist" + + if err := os.RemoveAll(distDir); err != nil { + return fmt.Errorf("removing dist directory: %w", err) + } + + if err := os.MkdirAll(distDir, 0755); err != nil { + return fmt.Errorf("creating dist directory: %w", err) + } + + if err := os.CopyFS(distDir, os.DirFS(buildDir)); err != nil { + return fmt.Errorf("copying build artifacts to %s: %w", distDir, err) + } + + draftFlag := "" + if cfg.GetDeliverables().GetGoGithubRelease().GetDraft() { + draftFlag = "--draft" + } + + args := []string{"bash", "-c", fmt.Sprintf(` + gh release create v%s %s --generate-notes --verify-tag --latest ./dist/* + `, cfg.GetVersion(), draftFlag, + )} + + if _, err := taskutil.RunCommand(ctx, args); err != nil { + return err + } return nil } From 8adcaa815b9b12648fd2d135ad7f873f89b4cfd7 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 13:07:10 -0500 Subject: [PATCH 26/38] First try of conditionally checking out the repo in GHA --- .github/workflows/checkout.yaml | 23 ++++++++ .github/workflows/main.yaml | 96 ++++++++++----------------------- 2 files changed, 52 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/checkout.yaml diff --git a/.github/workflows/checkout.yaml b/.github/workflows/checkout.yaml new file mode 100644 index 0000000..4604d03 --- /dev/null +++ b/.github/workflows/checkout.yaml @@ -0,0 +1,23 @@ +# This reusable workflow allows for checking out the most "correct" ref from the repo based on the +# condition that triggered the caller Workflow + +name: "Conditional checkout" + +runs-on: + workflow_call: {} + +jobs: + checkout: + runs-on: "ubuntu-latest" + steps: + - name: "Checkout PR branch" + if: "github.event_name == 'pull_request'" + uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0 + with: + fetch-depth: 0 + ref: "${{ github.event.pull_request.head.ref }}" + - name: "Checkout main branch" + if: "github.ref == 'refs/heads/main'" + uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0 + with: + fetch-depth: 0 diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2afb7b4..c245d28 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,6 +15,7 @@ env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" MISE_GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + MAIN_REF: "refs/heads/main" jobs: ci: @@ -22,82 +23,43 @@ jobs: permissions: contents: "read" steps: - - name: "Checkout repository" - uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # release v5.0.0 + - name: "Checkout" + uses: "./.github/workflows/checkout.yaml" - name: "Install system packages" run: | sudo apt-get update sudo apt-get install -y \ make - name: "Set up mise" - uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # release v3.2.0 + uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # v3.2.0 with: version: "2025.9.10" install: true # runs `mise install` cache: true - name: "Run CI Tasks" run: "make ci" - # deliver: - # runs-on: "ubuntu-latest" - # permissions: - # contents: "read" - # packages: "write" - # steps: - # - name: "Checkout repository" - # uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # release v5.0.0 - # - name: "Install system packages" - # run: | - # sudo apt-get update - # sudo apt-get install -y \ - # make - # - name: "Set up mise" - # uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # release v3.2.0 - # with: - # version: "2025.9.10" - # install: true # runs `mise install` - # cache: true - # - name: "Run Delivery Tasks" - # run: "make deliver" - # env: - # GITHUB_TOKEN: "${{ github.token }}" - - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v2 - # - name: Log in to GHCR - # uses: docker/login-action@v2 - # with: - # registry: ghcr.io - # username: ${{ github.repository_owner }} - # password: ${{ github.token }} - - # # Generate any tags we want for the images - # # (https://github.com/docker/build-push-action/blob/master/docs/advanced/tags-labels.md) - # - name: Extract metadata (tags, labels) for OCI image - # id: meta - # uses: docker/metadata-action@v4 - # with: - # images: ghcr.io/opensourcecorp/oscar - # tags: | - # type=sha - - # # non-mainline - # - name: Build and push non-mainline OCI image - # if: github.ref != 'refs/heads/main' - # uses: docker/build-push-action@v3 - # with: - # context: . - # file: Containerfile - # push: true - # tags: - # - "latest" - # - "${VERSION}" - - # # mainline - # - name: Build and push mainline OCI image - # if: github.ref == 'refs/heads/main' - # uses: docker/build-push-action@v3 - # with: - # context: . - # file: Containerfile - # push: true - # tags: ghcr.io/opensourcecorp/oscar:latest + deliver: + runs-on: "ubuntu-latest" + permissions: + contents: "read" + packages: "write" + steps: + - name: "Checkout" + uses: "./.github/workflows/checkout.yaml" + - name: "Install system packages" + run: | + sudo apt-get update + sudo apt-get install -y \ + make + - name: "Set up mise" + uses: "jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566" # v3.2.0 + with: + version: "2025.9.10" + install: true # runs `mise install` + cache: true + - name: "Set up Docker" + if: "github.ref == '${{ env.MAIN_REF }}'" + uses: "docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435" # v3.11.1 + - name: "Run Delivery Tasks" + if: "github.ref == '${{ env.MAIN_REF }}'" + run: "make deliver" From 2a26af1fff0cf5fb1dfe376ec917b448c3c82162 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 13:09:18 -0500 Subject: [PATCH 27/38] oops --- .github/workflows/checkout.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checkout.yaml b/.github/workflows/checkout.yaml index 4604d03..9672f60 100644 --- a/.github/workflows/checkout.yaml +++ b/.github/workflows/checkout.yaml @@ -3,7 +3,7 @@ name: "Conditional checkout" -runs-on: +on: workflow_call: {} jobs: From 5a3fa7c07901a731bff48af3a40cf3fa8da6a1e1 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 13:12:29 -0500 Subject: [PATCH 28/38] Empty commit to trigger workflow From 00af898a5385d6fa3d97ff9bd7d438a2a6abf201 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 13:14:05 -0500 Subject: [PATCH 29/38] so stupid --- .github/workflows/checkout.yaml | 23 ----------------------- .github/workflows/main.yaml | 26 ++++++++++++++++++++++---- 2 files changed, 22 insertions(+), 27 deletions(-) delete mode 100644 .github/workflows/checkout.yaml diff --git a/.github/workflows/checkout.yaml b/.github/workflows/checkout.yaml deleted file mode 100644 index 9672f60..0000000 --- a/.github/workflows/checkout.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# This reusable workflow allows for checking out the most "correct" ref from the repo based on the -# condition that triggered the caller Workflow - -name: "Conditional checkout" - -on: - workflow_call: {} - -jobs: - checkout: - runs-on: "ubuntu-latest" - steps: - - name: "Checkout PR branch" - if: "github.event_name == 'pull_request'" - uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0 - with: - fetch-depth: 0 - ref: "${{ github.event.pull_request.head.ref }}" - - name: "Checkout main branch" - if: "github.ref == 'refs/heads/main'" - uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0 - with: - fetch-depth: 0 diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index c245d28..1747e5a 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -23,8 +23,17 @@ jobs: permissions: contents: "read" steps: - - name: "Checkout" - uses: "./.github/workflows/checkout.yaml" + - name: "Checkout PR branch" + if: "github.event_name == 'pull_request'" + uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0 + with: + fetch-depth: 0 + ref: "${{ github.event.pull_request.head.ref }}" + - name: "Checkout main branch" + if: "github.ref == 'refs/heads/main'" + uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0 + with: + fetch-depth: 0 - name: "Install system packages" run: | sudo apt-get update @@ -44,8 +53,17 @@ jobs: contents: "read" packages: "write" steps: - - name: "Checkout" - uses: "./.github/workflows/checkout.yaml" + - name: "Checkout PR branch" + if: "github.event_name == 'pull_request'" + uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0 + with: + fetch-depth: 0 + ref: "${{ github.event.pull_request.head.ref }}" + - name: "Checkout main branch" + if: "github.ref == 'refs/heads/main'" + uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0 + with: + fetch-depth: 0 - name: "Install system packages" run: | sudo apt-get update From 82eab43660005ce6251d617778da14431403824e Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 13:18:05 -0500 Subject: [PATCH 30/38] Pull up conditional for delivery tasks --- .github/workflows/main.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1747e5a..422644d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -48,9 +48,10 @@ jobs: - name: "Run CI Tasks" run: "make ci" deliver: + if: "github.ref == '${{ env.MAIN_REF }}'" runs-on: "ubuntu-latest" permissions: - contents: "read" + contents: "write" packages: "write" steps: - name: "Checkout PR branch" @@ -76,8 +77,6 @@ jobs: install: true # runs `mise install` cache: true - name: "Set up Docker" - if: "github.ref == '${{ env.MAIN_REF }}'" uses: "docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435" # v3.11.1 - name: "Run Delivery Tasks" - if: "github.ref == '${{ env.MAIN_REF }}'" run: "make deliver" From 87db84a8c73bb0aeb50f93b552f18fbd0f7f10ab Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 13:20:33 -0500 Subject: [PATCH 31/38] jfc --- .github/workflows/main.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 422644d..336efe3 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,7 +15,6 @@ env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" MISE_GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - MAIN_REF: "refs/heads/main" jobs: ci: @@ -48,7 +47,7 @@ jobs: - name: "Run CI Tasks" run: "make ci" deliver: - if: "github.ref == '${{ env.MAIN_REF }}'" + if: "github.ref == 'refs/heads/main'" runs-on: "ubuntu-latest" permissions: contents: "write" From d906115f05e403f6d5f06ca0287233d4273f0629 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 13:21:29 -0500 Subject: [PATCH 32/38] Ok now temp-test perms etc while it's not on the main branch --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 336efe3..a3e4526 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -47,7 +47,7 @@ jobs: - name: "Run CI Tasks" run: "make ci" deliver: - if: "github.ref == 'refs/heads/main'" + if: "github.ref == 'refs/heads/feature/wip'" runs-on: "ubuntu-latest" permissions: contents: "write" From 5ded2a7d787d8280c6680be71b87c2a1a7c03f61 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 13:22:51 -0500 Subject: [PATCH 33/38] oook --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index a3e4526..c0189fa 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -47,7 +47,7 @@ jobs: - name: "Run CI Tasks" run: "make ci" deliver: - if: "github.ref == 'refs/heads/feature/wip'" + if: "github.ref != 'refs/heads/main'" runs-on: "ubuntu-latest" permissions: contents: "write" From f969ff4a25975fa2efeb053348568cd8d40d7f2c Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 13:44:31 -0500 Subject: [PATCH 34/38] GHA says the image build & push succeeded but I have no idea where it is?? --- .github/workflows/main.yaml | 2 +- .../opensourcecorp/oscar/config/v1/config.pb.go | 10 +++++----- internal/oscarcfg/test.oscar.yaml | 2 +- internal/tasks/tools/containers/deliver.go | 12 +++++++----- oscar.yaml | 2 +- proto/opensourcecorp/oscar/config/v1/config.proto | 4 ++-- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index c0189fa..2bd333d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -78,4 +78,4 @@ jobs: - name: "Set up Docker" uses: "docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435" # v3.11.1 - name: "Run Delivery Tasks" - run: "make deliver" + run: "OSC_DEBUG=true make deliver" diff --git a/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go b/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go index 2de1088..2e21706 100644 --- a/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go +++ b/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go @@ -206,11 +206,11 @@ type ContainerImage struct { // // Example: "opensourcecorp" Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` - // The target OCI repository. May contain as many subpaths to the actual image artifact as + // The target OCI image name. May contain as many subpaths to the actual image artifact as // necessary based on the registry, e.g. "my-repo/my-image-group/my-image". // // Example: "oscar" - Repo string `protobuf:"bytes,3,opt,name=repo,proto3" json:"repo,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -259,9 +259,9 @@ func (x *ContainerImage) GetOwner() string { return "" } -func (x *ContainerImage) GetRepo() string { +func (x *ContainerImage) GetName() string { if x != nil { - return x.Repo + return x.Name } return "" } @@ -283,7 +283,7 @@ const file_opensourcecorp_oscar_config_v1_config_proto_rawDesc = "" + "\x0eContainerImage\x12\"\n" + "\bregistry\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bregistry\x12\x1c\n" + "\x05owner\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x05owner\x12\x1a\n" + - "\x04repo\x18\x03 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x04repoB`Z^github.com/opensourcecorp/oscar/internal/generated/opensourcecorp/oscar/config/v1;oscarcfgpbv1b\x06proto3" + "\x04name\x18\x03 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x04nameB`Z^github.com/opensourcecorp/oscar/internal/generated/opensourcecorp/oscar/config/v1;oscarcfgpbv1b\x06proto3" var ( file_opensourcecorp_oscar_config_v1_config_proto_rawDescOnce sync.Once diff --git a/internal/oscarcfg/test.oscar.yaml b/internal/oscarcfg/test.oscar.yaml index bf2a63d..65735a5 100644 --- a/internal/oscarcfg/test.oscar.yaml +++ b/internal/oscarcfg/test.oscar.yaml @@ -8,4 +8,4 @@ deliverables: container_image: registry: "ghcr.io" owner: "opensourcecorp" - repo: "oscar" + name: "oscar" diff --git a/internal/tasks/tools/containers/deliver.go b/internal/tasks/tools/containers/deliver.go index 7ebc038..2433c86 100644 --- a/internal/tasks/tools/containers/deliver.go +++ b/internal/tasks/tools/containers/deliver.go @@ -92,13 +92,14 @@ func (t imageBuildPush) Exec(ctx context.Context) error { } // GROSS, DUDE - composeFile["services"].(map[string]any)[cfg.Repo].(map[string]any)["image"] = uri - composeFile["services"].(map[string]any)[cfg.Repo].(map[string]any)["build"].(map[string]any)["context"] = curDir + composeFile["services"].(map[string]any)[cfg.GetName()].(map[string]any)["image"] = uri + composeFile["services"].(map[string]any)[cfg.GetName()].(map[string]any)["build"].(map[string]any)["context"] = curDir composeOut, err := yaml.Marshal(composeFile) if err != nil { return err } + iprint.Debugf("edited Compose file YAML: %s\n", string(composeOut)) workDir := filepath.Join(os.TempDir(), "oscar-oci") if err := os.MkdirAll(workDir, 0755); err != nil { @@ -110,7 +111,7 @@ func (t imageBuildPush) Exec(ctx context.Context) error { return err } - registryMap := newRegistryMap(cfg.Repo) + registryMap := newRegistryMap(cfg.GetName()) var authArgs []string if strings.Contains(cfg.Registry, "ghcr") { @@ -123,7 +124,7 @@ func (t imageBuildPush) Exec(ctx context.Context) error { buildPushArgs := []string{"bash", "-c", fmt.Sprintf(` docker compose --file %s build --push %s - `, outPath, cfg.Repo, + `, outPath, cfg.GetName(), )} if _, err := taskutil.RunCommand(ctx, buildPushArgs); err != nil { return err @@ -153,8 +154,9 @@ func constructImageURI(ctx context.Context, rootCfg *oscarcfgpbv1.Config) (strin uri := fmt.Sprintf( "%s/%s/%s:%s", - cfg.GetRegistry(), cfg.GetOwner(), cfg.GetRepo(), tag, + cfg.GetRegistry(), cfg.GetOwner(), cfg.GetName(), tag, ) + iprint.Debugf("image URI: %s\n", uri) return uri, nil } diff --git a/oscar.yaml b/oscar.yaml index 2f0dbb0..e75b6ca 100644 --- a/oscar.yaml +++ b/oscar.yaml @@ -8,4 +8,4 @@ deliverables: container_image: registry: "ghcr.io" owner: "opensourcecorp" - repo: "oscar" + name: "oscar" diff --git a/proto/opensourcecorp/oscar/config/v1/config.proto b/proto/opensourcecorp/oscar/config/v1/config.proto index bccfe80..acc8f4a 100644 --- a/proto/opensourcecorp/oscar/config/v1/config.proto +++ b/proto/opensourcecorp/oscar/config/v1/config.proto @@ -47,9 +47,9 @@ message ContainerImage { // // Example: "opensourcecorp" string owner = 2 [(buf.validate.field).required = true]; - // The target OCI repository. May contain as many subpaths to the actual image artifact as + // The target OCI image name. May contain as many subpaths to the actual image artifact as // necessary based on the registry, e.g. "my-repo/my-image-group/my-image". // // Example: "oscar" - string repo = 3 [(buf.validate.field).required = true]; + string name = 3 [(buf.validate.field).required = true]; } From d381688eb84e98141f314d9624e44b31d17c3671 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 14:12:59 -0500 Subject: [PATCH 35/38] Remove debug logs and see if an existing GH Package can be pushed to, since I ran it locally successfully --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2bd333d..c0189fa 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -78,4 +78,4 @@ jobs: - name: "Set up Docker" uses: "docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435" # v3.11.1 - name: "Run Delivery Tasks" - run: "OSC_DEBUG=true make deliver" + run: "make deliver" From 399753a7d30da46fdb0a48242f619e06ef93da9e Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 14:49:17 -0500 Subject: [PATCH 36/38] k well maybe it's because it's not on the main branch, idk, I'll figure it out later --- .github/workflows/main.yaml | 2 +- .../opensourcecorp/oscar/config/v1/config.pb.go | 14 +++++++------- internal/oscarcfg/test.oscar.yaml | 2 +- internal/tasks/tools/containers/deliver.go | 2 +- oscar.yaml | 2 +- proto/opensourcecorp/oscar/config/v1/config.proto | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index c0189fa..336efe3 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -47,7 +47,7 @@ jobs: - name: "Run CI Tasks" run: "make ci" deliver: - if: "github.ref != 'refs/heads/main'" + if: "github.ref == 'refs/heads/main'" runs-on: "ubuntu-latest" permissions: contents: "write" diff --git a/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go b/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go index 2e21706..252d1c4 100644 --- a/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go +++ b/internal/generated/opensourcecorp/oscar/config/v1/config.pb.go @@ -202,10 +202,10 @@ type ContainerImage struct { // // Example: "ghcr.io" Registry string `protobuf:"bytes,1,opt,name=registry,proto3" json:"registry,omitempty"` - // The target OCI repository name. + // The target OCI namespace. // // Example: "opensourcecorp" - Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` // The target OCI image name. May contain as many subpaths to the actual image artifact as // necessary based on the registry, e.g. "my-repo/my-image-group/my-image". // @@ -252,9 +252,9 @@ func (x *ContainerImage) GetRegistry() string { return "" } -func (x *ContainerImage) GetOwner() string { +func (x *ContainerImage) GetNamespace() string { if x != nil { - return x.Owner + return x.Namespace } return "" } @@ -279,10 +279,10 @@ const file_opensourcecorp_oscar_config_v1_config_proto_rawDesc = "" + "\x0fcontainer_image\x18\x02 \x01(\v2..opensourcecorp.oscar.config.v1.ContainerImageR\x0econtainerImage\"T\n" + "\x0fGoGitHubRelease\x12+\n" + "\rbuild_sources\x18\x01 \x03(\tB\x06\xbaH\x03\xc8\x01\x01R\fbuildSources\x12\x14\n" + - "\x05draft\x18\x02 \x01(\bR\x05draft\"n\n" + + "\x05draft\x18\x02 \x01(\bR\x05draft\"v\n" + "\x0eContainerImage\x12\"\n" + - "\bregistry\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bregistry\x12\x1c\n" + - "\x05owner\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x05owner\x12\x1a\n" + + "\bregistry\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bregistry\x12$\n" + + "\tnamespace\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\tnamespace\x12\x1a\n" + "\x04name\x18\x03 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x04nameB`Z^github.com/opensourcecorp/oscar/internal/generated/opensourcecorp/oscar/config/v1;oscarcfgpbv1b\x06proto3" var ( diff --git a/internal/oscarcfg/test.oscar.yaml b/internal/oscarcfg/test.oscar.yaml index 65735a5..61f8f2f 100644 --- a/internal/oscarcfg/test.oscar.yaml +++ b/internal/oscarcfg/test.oscar.yaml @@ -7,5 +7,5 @@ deliverables: draft: false container_image: registry: "ghcr.io" - owner: "opensourcecorp" + namespace: "opensourcecorp" name: "oscar" diff --git a/internal/tasks/tools/containers/deliver.go b/internal/tasks/tools/containers/deliver.go index 2433c86..0737ebe 100644 --- a/internal/tasks/tools/containers/deliver.go +++ b/internal/tasks/tools/containers/deliver.go @@ -154,7 +154,7 @@ func constructImageURI(ctx context.Context, rootCfg *oscarcfgpbv1.Config) (strin uri := fmt.Sprintf( "%s/%s/%s:%s", - cfg.GetRegistry(), cfg.GetOwner(), cfg.GetName(), tag, + cfg.GetRegistry(), cfg.GetNamespace(), cfg.GetName(), tag, ) iprint.Debugf("image URI: %s\n", uri) diff --git a/oscar.yaml b/oscar.yaml index e75b6ca..e49dd07 100644 --- a/oscar.yaml +++ b/oscar.yaml @@ -7,5 +7,5 @@ deliverables: draft: false container_image: registry: "ghcr.io" - owner: "opensourcecorp" + namespace: "opensourcecorp" name: "oscar" diff --git a/proto/opensourcecorp/oscar/config/v1/config.proto b/proto/opensourcecorp/oscar/config/v1/config.proto index acc8f4a..c918e71 100644 --- a/proto/opensourcecorp/oscar/config/v1/config.proto +++ b/proto/opensourcecorp/oscar/config/v1/config.proto @@ -43,10 +43,10 @@ message ContainerImage { // // Example: "ghcr.io" string registry = 1 [(buf.validate.field).required = true]; - // The target OCI repository name. + // The target OCI namespace. // // Example: "opensourcecorp" - string owner = 2 [(buf.validate.field).required = true]; + string namespace = 2 [(buf.validate.field).required = true]; // The target OCI image name. May contain as many subpaths to the actual image artifact as // necessary based on the registry, e.g. "my-repo/my-image-group/my-image". // From 143d39850931df85306ce1ee4dfe431b18a06a29 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 14:50:31 -0500 Subject: [PATCH 37/38] Tidy up README, for now --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index a28a6ba..9aeb900 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,5 @@ wheel-reinvention. * `self-update` subcommand * CI additions * Terraform - * protobuf - * R? - * Rust? * CD additions - * GitHub Releases - * Publish to ghcr + * Publishing to ghcr is confirmed to be working when run on `main` branch From a166a5c3b7cf358f5868108bc8e5bed441452f37 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 21 Sep 2025 15:04:33 -0500 Subject: [PATCH 38/38] Tidy up what I found reviewing the PR diff --- README.md | 1 + internal/git/git.go | 17 ++++++++++++----- internal/git/git_test.go | 2 +- internal/tasks/tools/containers/deliver.go | 7 +++++-- internal/tasks/tools/go/deliver.go | 2 ++ internal/tasks/util/core_types.go | 2 +- mise.toml | 1 - proto/buf.gen.yaml | 1 + 8 files changed, 23 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9aeb900..604132a 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ wheel-reinvention. * Also have it dump its own `mise.toml` for the user * `self-update` subcommand * CI additions + * Protobuf (especially since there's proto code in this repo now) * Terraform * CD additions * Publishing to ghcr is confirmed to be working when run on `main` branch diff --git a/internal/git/git.go b/internal/git/git.go index 98ff2dd..a479265 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -13,16 +13,23 @@ import ( // Git holds metadata about the current state of the Git repository. type Git struct { - Root string - Branch string - LatestTag string + // The root directory of the repository on the host. + Root string + // The current branch name. + Branch string + // The latest tag available in the repo. + LatestTag string + // The latest commit on the current branch. LatestCommit string - IsDirty bool + // Whether or not the working directory has uncommitted changes. + IsDirty bool } // Status holds various pieces of information about Git status. type Status struct { - Diff []string + // The list of modified files. + Diff []string + // The list of untracked files. UntrackedFiles []string } diff --git a/internal/git/git_test.go b/internal/git/git_test.go index 588633a..c7cd3e1 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestBranchForURI(t *testing.T) { +func TestSanitizedBranch(t *testing.T) { want := "feature-wip" tt := []struct { diff --git a/internal/tasks/tools/containers/deliver.go b/internal/tasks/tools/containers/deliver.go index 0737ebe..634f9ba 100644 --- a/internal/tasks/tools/containers/deliver.go +++ b/internal/tasks/tools/containers/deliver.go @@ -19,15 +19,18 @@ type ( imageBuildPush struct{ taskutil.Tool } ) +// registryMapping contains substructs to be used based on the target OCI registry. type registryMapping struct { - Name string GitHub gitHubRegistry } +// gitHubRegistry provides fields for use in targeting "ghcr.io". type gitHubRegistry struct { + // The command to run to authenticate to the registry. AuthCommand []string } +// newRegistryMap returns a populated [registryMapping]. func newRegistryMap(username string) registryMapping { return registryMapping{ GitHub: gitHubRegistry{ @@ -136,7 +139,7 @@ func (t imageBuildPush) Exec(ctx context.Context) error { // Post implements [taskutil.Tasker.Post]. func (t imageBuildPush) Post(_ context.Context) error { return nil } -// TODO +// constructImageURI constructs an image URI based on data from oscar's config & Git. func constructImageURI(ctx context.Context, rootCfg *oscarcfgpbv1.Config) (string, error) { cfg := rootCfg.GetDeliverables().GetContainerImage() git, err := igit.New(ctx) diff --git a/internal/tasks/tools/go/deliver.go b/internal/tasks/tools/go/deliver.go index e004d2f..6aeb5e7 100644 --- a/internal/tasks/tools/go/deliver.go +++ b/internal/tasks/tools/go/deliver.go @@ -88,6 +88,8 @@ func (t ghRelease) Exec(ctx context.Context) error { // Post implements [taskutil.Tasker.Post]. func (t ghRelease) Post(_ context.Context) error { return nil } +// goBuild cross-compiles the provided source package and places the resulting artifacts in a +// root-level "build/" subdirectory. func goBuild(ctx context.Context, src string) error { if strings.HasSuffix(src, ".go") { return fmt.Errorf("provided Go build source '%s' was a file, but must be a path to a package", src) diff --git a/internal/tasks/util/core_types.go b/internal/tasks/util/core_types.go index 00f8126..5b9415b 100644 --- a/internal/tasks/util/core_types.go +++ b/internal/tasks/util/core_types.go @@ -24,7 +24,7 @@ type Tasker interface { // represent a task that runs "go test", but you *would* need a tool to represent a task that runs // the external "staticcheck" linter for Go. type Tool struct { - // TODO + // The list of command & arguments to run during [Tasker.Exec]. RunArgs []string // The path to the tool's config file, if it has one to use. ConfigFilePath string diff --git a/mise.toml b/mise.toml index ef82227..d1358f7 100644 --- a/mise.toml +++ b/mise.toml @@ -26,7 +26,6 @@ yamllint = "1.37.1" "go:golang.org/x/tools/cmd/goimports" = { version = "0.37.0" } "go:golang.org/x/vuln/cmd/govulncheck" = { version = "1.1.4" } "go:google.golang.org/protobuf/cmd/protoc-gen-go" = { version = "1.36.8" } -# gets mad about Go toolchain versions if not "latest" "go:honnef.co/go/tools/cmd/staticcheck" = { version = "2025.1.1" } ### Python-specific tools. Note that others that may also be CLI tools, may be run internally via diff --git a/proto/buf.gen.yaml b/proto/buf.gen.yaml index e257007..f120073 100644 --- a/proto/buf.gen.yaml +++ b/proto/buf.gen.yaml @@ -7,6 +7,7 @@ plugins: out: "../internal/generated" opt: - "paths=source_relative" + # TODO: implement # # ../proto/generate.go # - local: "../build/protoc-gen-oscarcfg" # out: "../internal/generated"