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
29 changes: 29 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Build

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: {}

jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build
run: make build
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: po-${{ matrix.os }}
path: bin/po*
22 changes: 22 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Lint

on:
pull_request:
branches: [main]

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: latest
28 changes: 28 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Unit Tests

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: {}

jobs:
test:
name: Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: make test
- name: Upload to Codecov
uses: codecov/codecov-action@v5
if: always()
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: coverage.out
fail_ci_if_error: false
37 changes: 37 additions & 0 deletions .github/workflows/verify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Verify

on:
pull_request:
branches: [main]

jobs:
verify:
name: Verify
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Check formatting
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "Go files are not formatted:"
gofmt -d .
exit 1
fi
- name: Go vet
run: go vet ./...
- name: Check dirty tree
run: |
git update-index --refresh
git diff-index --cached --quiet --ignore-submodules HEAD --
git diff-files --quiet --ignore-submodules
git diff --exit-code HEAD --
STATUS=$(git status -s)
if [ -n "$STATUS" ]; then
echo "Untracked files detected:"
echo "$STATUS"
exit 1
fi
20 changes: 20 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: "2"

run:
timeout: 5m
go: "1.25"

linters:
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- misspell
- revive

formatters:
enable:
- gofmt
- goimports
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@ DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS = -X github.com/wangke19/po/internal/build.Version=$(VERSION) \
-X github.com/wangke19/po/internal/build.Date=$(DATE)

.PHONY: build test install clean
.PHONY: build test install clean fmt vet lint

build:
go build -ldflags "$(LDFLAGS)" -o bin/po ./cmd/po

test:
go test ./...
go test -race -coverprofile=coverage.out -covermode=atomic ./...

install:
go install -ldflags "$(LDFLAGS)" ./cmd/po

clean:
rm -rf bin/

fmt:
gofmt -s -w .

vet:
go vet ./...

lint:
golangci-lint run
1 change: 1 addition & 0 deletions cmd/po/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package main is the entry point for the po CLI.
package main

import (
Expand Down
2 changes: 2 additions & 0 deletions internal/build/build.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Package build provides version information for the po CLI.
package build

// Version information set by ldflags at build time.
var (
Version = "dev"
Date = "unknown"
Expand Down
11 changes: 11 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package config manages Polarion CLI configuration and authentication.
package config

import (
Expand All @@ -18,17 +19,20 @@ type configFile struct {
Hosts map[string]hostEntry `yaml:"hosts"`
}

// Config holds Polarion CLI configuration.
type Config struct {
path string
data configFile
}

// New creates a new Config instance.
func New(path string) *Config {
c := &Config{path: path}
_ = c.load()
return c
}

// DefaultConfigPath returns the default configuration file path.
func DefaultConfigPath() string {
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return filepath.Join(xdg, "po", "config.yml")
Expand All @@ -37,6 +41,7 @@ func DefaultConfigPath() string {
return filepath.Join(home, ".config", "po", "config.yml")
}

// NormalizeHostname normalizes a Polarion hostname.
func NormalizeHostname(host string) string {
host = strings.TrimPrefix(host, "https://")
host = strings.TrimPrefix(host, "http://")
Expand All @@ -62,6 +67,7 @@ func (c *Config) save() error {
return os.WriteFile(c.path, data, 0o600)
}

// SetHost configures a Polarion host.
func (c *Config) SetHost(hostname, project string, verifySSL bool) error {
hostname = NormalizeHostname(hostname)
if c.data.Hosts == nil {
Expand All @@ -71,12 +77,14 @@ func (c *Config) SetHost(hostname, project string, verifySSL bool) error {
return c.save()
}

// RemoveHost removes a configured host.
func (c *Config) RemoveHost(hostname string) error {
hostname = NormalizeHostname(hostname)
delete(c.data.Hosts, hostname)
return c.save()
}

// DefaultHost returns the default Polarion host.
func (c *Config) DefaultHost() (string, error) {
if v := os.Getenv("POLARION_URL"); v != "" {
return NormalizeHostname(v), nil
Expand All @@ -87,6 +95,7 @@ func (c *Config) DefaultHost() (string, error) {
return "", fmt.Errorf("not logged in to any Polarion instance; run: po auth login")
}

// DefaultProject returns the default project for a host.
func (c *Config) DefaultProject(hostname string) (string, error) {
if v := os.Getenv("POLARION_PROJECT"); v != "" {
return v, nil
Expand All @@ -98,6 +107,7 @@ func (c *Config) DefaultProject(hostname string) (string, error) {
return "", fmt.Errorf("no project configured for %s", hostname)
}

// VerifySSL returns whether SSL verification is enabled for a host.
func (c *Config) VerifySSL(hostname string) bool {
if v := os.Getenv("POLARION_VERIFY_SSL"); v == "false" {
return false
Expand All @@ -109,6 +119,7 @@ func (c *Config) VerifySSL(hostname string) bool {
return true
}

// Hosts returns all configured hosts.
func (c *Config) Hosts() []string {
hosts := make([]string, 0, len(c.data.Hosts))
for h := range c.data.Hosts {
Expand Down
17 changes: 11 additions & 6 deletions internal/pocmd/cmd.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package pocmd provides the command execution logic for the po CLI.
package pocmd

import (
Expand All @@ -9,20 +10,24 @@ import (
"github.com/wangke19/po/pkg/cmdutil"
)

type exitCode int
// ExitCode represents the exit status of the command.
type ExitCode int

const (
exitOK exitCode = 0
exitError exitCode = 1
// ExitOK indicates successful execution.
ExitOK ExitCode = 0
// ExitError indicates an error occurred.
ExitError ExitCode = 1
)

func Main() exitCode {
// Main executes the root command and returns the exit code.
func Main() ExitCode {
f := cmdutil.New(build.Version)
cmd := root.NewCmdRoot(f, build.Version)

if err := cmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
return exitError
return ExitError
}
return exitOK
return ExitOK
}
1 change: 1 addition & 0 deletions pkg/browser/browser.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package browser provides utilities for opening URLs in the system browser.
package browser

import (
Expand Down
25 changes: 13 additions & 12 deletions pkg/cmd/api/api.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package api provides the api command for making authenticated Polarion API requests.
package api

import (
Expand All @@ -24,16 +25,17 @@ type apiOptions struct {
inputFile string
}

func NewCmdApi(f *cmdutil.Factory) *cobra.Command {
// NewCmdAPI returns the 'api' command for making authenticated Polarion API requests.
func NewCmdAPI(f *cmdutil.Factory) *cobra.Command {
opts := &apiOptions{}

cmd := &cobra.Command{
Use: "api <endpoint>",
Short: "Make an authenticated Polarion API request",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(_ *cobra.Command, args []string) error {
opts.endpoint = args[0]
return runApi(f, opts)
return runAPI(f, opts)
},
}

Expand All @@ -46,7 +48,7 @@ func NewCmdApi(f *cmdutil.Factory) *cobra.Command {
return cmd
}

func runApi(f *cmdutil.Factory, opts *apiOptions) error {
func runAPI(f *cmdutil.Factory, opts *apiOptions) error {
cfg, err := f.Config()
if err != nil {
return err
Expand All @@ -57,12 +59,11 @@ func runApi(f *cmdutil.Factory, opts *apiOptions) error {
return err
}

var project string
project, err = cfg.DefaultProject(host)
project, err := cfg.DefaultProject(host)
if err != nil && strings.Contains(opts.endpoint, "{project}") {
return fmt.Errorf("no project configured: use POLARION_PROJECT or po auth login")
}
err = nil // reset: missing project is non-fatal when {project} not in endpoint
// Note: missing project is non-fatal when {project} not in endpoint

token := os.Getenv("POLARION_TOKEN")
if token == "" {
Expand All @@ -72,7 +73,7 @@ func runApi(f *cmdutil.Factory, opts *apiOptions) error {
}
}

httpClient, err := f.HttpClient()
httpClient, err := f.HTTPClient()
if err != nil {
return err
}
Expand Down Expand Up @@ -126,7 +127,7 @@ func buildBody(opts *apiOptions) (io.Reader, error) {
if err != nil {
return nil, fmt.Errorf("open input file: %w", err)
}
defer f.Close()
defer func() { _ = f.Close() }()
data, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("read input file: %w", err)
Expand Down Expand Up @@ -173,7 +174,7 @@ func doRequest(ctx context.Context, client *http.Client, method, url, token stri
if err != nil {
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

data, err := io.ReadAll(resp.Body)
if err != nil {
Expand Down Expand Up @@ -225,11 +226,11 @@ func printJSON(f *cmdutil.Factory, data []byte) error {
var v any
if err := json.Unmarshal(data, &v); err == nil {
if pretty, err := json.MarshalIndent(v, "", " "); err == nil {
fmt.Fprintln(f.IOStreams.Out, string(pretty))
_, _ = fmt.Fprintln(f.IOStreams.Out, string(pretty))
return nil
}
}
}
fmt.Fprintln(f.IOStreams.Out, string(data))
_, _ = fmt.Fprintln(f.IOStreams.Out, string(data))
return nil
}
Loading
Loading