Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ 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: FORCE
@$(RUN) go build -o ./build/oscar ./cmd/oscar
@$(RUN) go build -ldflags '-s -w -extldflags "-static"' -o ./build/$(BINNAME) ./cmd/$(BINNAME)
@upx --best ./build/$(BINNAME)

clean: FORCE
@rm -rf \
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ TODO
<!-- | <empty cell> | <third target for same artifact type> | -->
<!-- | <second artifact type> | <first target for second artifact type> | -->

## Installation

`oscar` can be installed a few different ways:

* Downloading a binary from a [GitHub Release](https://github.com/opensourcecorp/oscar/releases).

* Via `mise`, using a `github` or `go` backend:

mise use "github:opensourcecorp/oscar@<version>"
# or
mise use "go:github.com/opensourcecorp/oscar/cmd/oscar@<version>"

* Via the Go toolchain:

go install github.com/opensourcecorp/oscar/cmd/oscar@<version>

## Requirements

Before getting started, note that `oscar` has a few host-system runtime dependencies. Some of these
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
buf.build/go/protovalidate v1.0.0
github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v4 v4.0.0-rc.2
golang.org/x/term v0.35.0
google.golang.org/protobuf v1.36.9
)

Expand All @@ -23,6 +24,7 @@ require (
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/sys v0.36.0 // 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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/y
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/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
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=
Expand Down
43 changes: 27 additions & 16 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import (

const (
// Command names and their flags
rootCmdName = "oscar"
debugFlagName = "debug"
rootCmdName = "oscar"
debugFlagName = "debug"
noBannerFlagName = "no-banner"
noColorFlagName = "no-color"

ciCommandName = "ci"

Expand All @@ -40,8 +42,27 @@ func NewRootCmd() *cli.Command {
Flags: []cli.Flag{
&cli.BoolFlag{
Name: debugFlagName,
Usage: "Whether to print debug logs during oscar runs",
Sources: cli.EnvVars(consts.DebugEnvVarName),
Usage: "Whether to print debug logs during oscar runs.",
Sources: cli.EnvVars(consts.OscarEnvVarDebug),
Action: func(_ context.Context, _ *cli.Command, _ bool) error {
return os.Setenv(consts.OscarEnvVarDebug, "true")
},
},
&cli.BoolFlag{
Name: noBannerFlagName,
Usage: "Pass to suppress printing oscar's stylistic banner at startup.",
Sources: cli.EnvVars(consts.OscarEnvVarNoBanner),
Action: func(_ context.Context, _ *cli.Command, _ bool) error {
return os.Setenv(consts.OscarEnvVarNoBanner, "true")
},
},
&cli.BoolFlag{
Name: noColorFlagName,
Usage: "Pass to suppress printing colored terminal output. Note that oscar defaults to printing in color during interactive runs.",
Sources: cli.EnvVars(consts.OscarEnvVarNoColor),
Action: func(_ context.Context, _ *cli.Command, _ bool) error {
return os.Setenv(consts.OscarEnvVarNoColor, "true")
},
},
},
Commands: []*cli.Command{
Expand All @@ -61,13 +82,6 @@ func NewRootCmd() *cli.Command {
return cmd
}

// maybeSetDebug conditionally sets oscar's debug env var, so that other packages can use it.
func maybeSetDebug(cmd *cli.Command) {
if cmd.Bool(debugFlagName) || os.Getenv(consts.DebugEnvVarName) != "" {
_ = os.Setenv(consts.DebugEnvVarName, "true")
}
}

// getVersion retrieves the version of the codebase.
func getVersion() (string, error) {
cfg, err := oscarcfg.Get()
Expand All @@ -80,15 +94,13 @@ func getVersion() (string, error) {

// rootAction defines the logic for oscar's root command.
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 valid subcommand")
}

// ciAction defines the logic for oscar's ci subcommand.
func ciAction(ctx context.Context, cmd *cli.Command) error {
maybeSetDebug(cmd)
func ciAction(ctx context.Context, _ *cli.Command) error {
iprint.Banner()
iprint.Debugf("oscar ci subcommand\n")

Expand All @@ -100,8 +112,7 @@ func ciAction(ctx context.Context, cmd *cli.Command) error {
}

// deliverAction defines the logic for oscar's deliver subcommand.
func deliverAction(ctx context.Context, cmd *cli.Command) error {
maybeSetDebug(cmd)
func deliverAction(ctx context.Context, _ *cli.Command) error {
iprint.Banner()
iprint.Debugf("oscar deliver subcommand\n")

Expand Down
10 changes: 8 additions & 2 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ import (
)

const (
// DebugEnvVarName is for enabling debug logs etc.
DebugEnvVarName = "OSC_DEBUG"
// OscarEnvVarDebug is for enabling debug logs etc.
OscarEnvVarDebug = "OSC_DEBUG"

// OscarEnvVarNoBanner is used to suppress printing the stylistic banner at startup.
OscarEnvVarNoBanner = "OSCAR_NO_BANNER"

// OscarEnvVarNoColor is used to suppress printing colored terminal output.
OscarEnvVarNoColor = "OSCAR_NO_COLOR"

// 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.
Expand Down
12 changes: 6 additions & 6 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"

iprint "github.com/opensourcecorp/oscar/internal/print"
taskutil "github.com/opensourcecorp/oscar/internal/tasks/util"
"github.com/opensourcecorp/oscar/internal/system"
)

// Git holds metadata about the current state of the Git repository.
Expand All @@ -35,25 +35,25 @@ type Status struct {

// New returns a populated [Git].
func New(ctx context.Context) (*Git, error) {
root, err := taskutil.RunCommand(ctx, []string{"git", "rev-parse", "--show-toplevel"})
root, err := system.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"})
branch, err := system.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"})
latestTag, err := system.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"})
latestCommit, err := system.RunCommand(ctx, []string{"git", "rev-parse", "--short=8", "HEAD"})
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -107,7 +107,7 @@ func (g *Git) String() string {
// 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"})
outputBytes, err := system.RunCommand(ctx, []string{"git", "status", "--porcelain"})
if err != nil {
return Status{}, fmt.Errorf("getting git status output: %w", err)
}
Expand Down
91 changes: 91 additions & 0 deletions internal/print/colors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package iprint

// NOTE: there's a lot of extra-feeling code in here, but that's because of how these vars/functions
// end up being used. Since oscar checks for the '--no-color' flag at execution startup (and then
// sets the env var for it), but that env var is checked *at overall program start time* (i.e.
// before any flags are parsed/handled), we can't just set top-level global vars per color that
// check the env var -- we have to only return color codes on-demand, so the var is available to be
// checked at any point after the env var is assigned.
//
// All this is fine, it just might be goofy-looking.

import (
"os"

"github.com/opensourcecorp/oscar/internal/consts"
"golang.org/x/term"
)

var (
// ANSI color codes
reset = "\033[0m"
red = "\033[31m"
green = "\033[32m"
yellow = "\033[33m"
blue = "\033[34m"
magenta = "\033[35m"
cyan = "\033[36m"
gray = "\033[37m"
white = "\033[97m"
)

// AllColors holds All the possible ANSI color codes that can be used. See the topmost comment in
// this file for "why".
type AllColors struct {
Reset string
Red string
Green string
Yellow string
Blue string
Magenta string
Cyan string
Gray string
White string
DebugColor string
InfoColor string
WarnColor string
ErrorColor string
GoodColor string
}

// Colors returns a populated [AllColors]. It can be used to grab a conditional ANSI color code in
// message-printing.
//
// For example, to override a print function's default text color with, say, red, you could do
// something like this:
//
// fmt.Println(iprint.Colors().Red + "oops" + iprint.Colors().Reset)
func Colors() AllColors {
out := AllColors{
Reset: color(reset),
Red: color(red),
Green: color(green),
Yellow: color(yellow),
Blue: color(blue),
Magenta: color(magenta),
Cyan: color(cyan),
Gray: color(gray),
White: color(white),

DebugColor: color(blue),
InfoColor: color(cyan),
WarnColor: color(yellow),
ErrorColor: color(red),
GoodColor: color(green),
}

return out
}

// color is passed an ANSI color code, and conditionally returns either it or an empty string in the
// case of colors being disabled.
func color(ansiCode string) string {
if !term.IsTerminal(int(os.Stdout.Fd())) {
return ""
}
if noColor := os.Getenv(consts.OscarEnvVarNoColor); noColor != "" {
return ""
}

return ansiCode
}
2 changes: 1 addition & 1 deletion internal/print/doc.go
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Package iprint contains helper functions for printing information for the caller.
// Package iprint contains helper functions for printing information.
package iprint
54 changes: 45 additions & 9 deletions internal/print/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package iprint
import (
"fmt"
"os"
"time"

"github.com/opensourcecorp/oscar/internal/consts"
)

// Banner prints the oscar banner.
// Banner prints the oscar stylistic banner.
func Banner() {
var banner = `
____________________ \
Expand All @@ -17,29 +18,64 @@ func Banner() {
~=~=~=~=| |_| _| |_ | | | \ |/|---------/
|____________________|/ /
`
fmt.Println(banner)

if os.Getenv(consts.OscarEnvVarNoBanner) == "" {
Goodf(banner + "\n")
}
}

// Debugf is a helper function that prints debug logs if requested.
func Debugf(format string, args ...any) {
if os.Getenv(consts.DebugEnvVarName) != "" {
format = "DEBUG: " + format
fmt.Printf(format, args...)
colors := Colors()
if os.Getenv(consts.OscarEnvVarDebug) != "" {
fmt.Printf(colors.DebugColor+"DEBUG: "+format+colors.Reset, args...)
}
}

// Infof is a helper function that writes info-level text.
func Infof(format string, args ...any) {
colors := Colors()
fmt.Printf(colors.InfoColor+format+colors.Reset, args...)
}

// Warnf is a helper function that writes warnings to standard error.
func Warnf(format string, args ...any) {
if _, err := fmt.Fprintf(os.Stderr, "WARN: "+format, args...); err != nil {
colors := Colors()
if _, err := fmt.Fprintf(
os.Stderr,
colors.WarnColor+"WARN: "+format+colors.Reset,
args...,
); err != nil {
// NOTE: panicking is fine here, this would be catastrophic lol
panic(fmt.Sprintf("trying to write warning to stderr: %v", err))
panic(
fmt.Sprintf(colors.ErrorColor+"trying to write warning to stderr: %v"+colors.Reset, err),
)
}
}

// Errorf is a helper function that writes errors to standard error.
func Errorf(format string, args ...any) {
if _, err := fmt.Fprintf(os.Stderr, format, args...); err != nil {
colors := Colors()
if _, err := fmt.Fprintf(
os.Stderr,
colors.ErrorColor+format+colors.Reset,
args...,
); err != nil {
// NOTE: panicking is fine here, this would be catastrophic lol
panic(fmt.Sprintf("trying to write error to stderr: %v", err))
panic(
fmt.Sprintf(colors.ErrorColor+"trying to write error to stderr: %v"+colors.Reset, err),
)
}
}

// Goodf is a helper function that prints green info text indicating something went well
func Goodf(format string, args ...any) {
colors := Colors()
fmt.Printf(colors.GoodColor+format+colors.Reset, args...)
}

// 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())
}
2 changes: 2 additions & 0 deletions internal/system/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package system contains functionality for host-related activities.
package system
Loading