Merge pull request 'chore: setup CI' (#1) from ci_setup into main
Reviewed-on: https://codeberg.org/romaintb/fgj/pulls/1
This commit is contained in:
commit
a2d3858462
10 changed files with 373 additions and 14 deletions
56
.gitea/workflows/ci.yml
Normal file
56
.gitea/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: codeberg-small
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: https://github.com/actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
cache: true
|
||||
|
||||
- name: Install golangci-lint
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.6.2
|
||||
|
||||
- name: Run golangci-lint
|
||||
run: golangci-lint run --timeout 5m
|
||||
|
||||
build:
|
||||
runs-on: codeberg-small
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: https://github.com/actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
cache: true
|
||||
|
||||
- name: Build application
|
||||
run: make build
|
||||
|
||||
test:
|
||||
runs-on: codeberg-small
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: https://github.com/actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
cache: true
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v -race ./...
|
||||
13
.golangci.yml
Normal file
13
.golangci.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
tests: true
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- errcheck
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
29
Makefile
Normal file
29
Makefile
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
.PHONY: help build run test clean lint lint-fix
|
||||
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " make build - Build the application"
|
||||
@echo " make run - Run the application"
|
||||
@echo " make test - Run tests"
|
||||
@echo " make lint - Run golangci-lint"
|
||||
@echo " make lint-fix - Run golangci-lint with auto-fix"
|
||||
@echo " make clean - Clean build artifacts"
|
||||
|
||||
build:
|
||||
go build -o bin/fgj .
|
||||
|
||||
run:
|
||||
go run .
|
||||
|
||||
test:
|
||||
go test -v -race ./...
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
|
||||
lint-fix:
|
||||
golangci-lint run --fix ./...
|
||||
|
||||
clean:
|
||||
rm -rf bin/
|
||||
go clean
|
||||
|
|
@ -124,13 +124,13 @@ func runIssueList(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NUMBER\tTITLE\tSTATE\n")
|
||||
_, _ = fmt.Fprintf(w, "NUMBER\tTITLE\tSTATE\n")
|
||||
for _, issue := range issues {
|
||||
if issue.PullRequest == nil {
|
||||
fmt.Fprintf(w, "#%d\t%s\t%s\n", issue.Index, issue.Title, issue.State)
|
||||
_, _ = fmt.Fprintf(w, "#%d\t%s\t%s\n", issue.Index, issue.Title, issue.State)
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
_ = w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,11 +116,11 @@ func runPRList(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NUMBER\tTITLE\tBRANCH\tSTATE\n")
|
||||
_, _ = fmt.Fprintf(w, "NUMBER\tTITLE\tBRANCH\tSTATE\n")
|
||||
for _, pr := range prs {
|
||||
fmt.Fprintf(w, "#%d\t%s\t%s\t%s\n", pr.Index, pr.Title, pr.Head.Ref, pr.State)
|
||||
_, _ = fmt.Fprintf(w, "#%d\t%s\t%s\t%s\n", pr.Index, pr.Title, pr.Head.Ref, pr.State)
|
||||
}
|
||||
w.Flush()
|
||||
_ = w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ func runRepoList(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tVISIBILITY\tDESCRIPTION\n")
|
||||
_, _ = fmt.Fprintf(w, "NAME\tVISIBILITY\tDESCRIPTION\n")
|
||||
for _, repo := range repos {
|
||||
visibility := "public"
|
||||
if repo.Private {
|
||||
|
|
@ -137,9 +137,9 @@ func runRepoList(cmd *cobra.Command, args []string) error {
|
|||
if len(desc) > 50 {
|
||||
desc = desc[:47] + "..."
|
||||
}
|
||||
fmt.Fprintf(w, "%s/%s\t%s\t%s\n", repo.Owner.UserName, repo.Name, visibility, desc)
|
||||
_, _ = fmt.Fprintf(w, "%s/%s\t%s\t%s\n", repo.Owner.UserName, repo.Name, visibility, desc)
|
||||
}
|
||||
w.Flush()
|
||||
_ = w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func init() {
|
|||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/fgj/config.yaml)")
|
||||
rootCmd.PersistentFlags().String("hostname", "", "Forgejo instance hostname")
|
||||
viper.BindPFlag("hostname", rootCmd.PersistentFlags().Lookup("hostname"))
|
||||
_ = viper.BindPFlag("hostname", rootCmd.PersistentFlags().Lookup("hostname"))
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
|
|
@ -41,7 +41,7 @@ func initConfig() {
|
|||
}
|
||||
|
||||
configDir := home + "/.config/fgj"
|
||||
os.MkdirAll(configDir, 0755)
|
||||
_ = os.MkdirAll(configDir, 0755)
|
||||
|
||||
viper.AddConfigPath(configDir)
|
||||
viper.SetConfigType("yaml")
|
||||
|
|
@ -51,7 +51,5 @@ func initConfig() {
|
|||
viper.AutomaticEnv()
|
||||
viper.SetEnvPrefix("FGJ")
|
||||
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
// Config file found and successfully parsed
|
||||
}
|
||||
_ = viper.ReadInConfig()
|
||||
}
|
||||
|
|
|
|||
28
internal/api/client_test.go
Normal file
28
internal/api/client_test.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"codeberg.org/romaintb/fgj/internal/config"
|
||||
)
|
||||
|
||||
func TestClient_Hostname(t *testing.T) {
|
||||
client := &Client{
|
||||
hostname: "codeberg.org",
|
||||
}
|
||||
|
||||
if client.Hostname() != "codeberg.org" {
|
||||
t.Errorf("Expected hostname 'codeberg.org', got '%s'", client.Hostname())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientFromConfig_MissingHost(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Hosts: map[string]config.HostConfig{},
|
||||
}
|
||||
|
||||
_, err := NewClientFromConfig(cfg, "nonexistent.org")
|
||||
if err == nil {
|
||||
t.Error("Expected error for nonexistent host")
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,10 @@ func Load() (*Config, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return LoadFromPath(path)
|
||||
}
|
||||
|
||||
func LoadFromPath(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
|
|
@ -67,7 +70,10 @@ func (c *Config) Save() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SaveToPath(path)
|
||||
}
|
||||
|
||||
func (c *Config) SaveToPath(path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
|
|
|
|||
229
internal/config/config_test.go
Normal file
229
internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfig_SetHost(t *testing.T) {
|
||||
cfg := &Config{}
|
||||
|
||||
hostConfig := HostConfig{
|
||||
Hostname: "codeberg.org",
|
||||
Token: "test-token",
|
||||
User: "testuser",
|
||||
}
|
||||
|
||||
cfg.SetHost("codeberg.org", hostConfig)
|
||||
|
||||
if cfg.Hosts == nil {
|
||||
t.Fatal("Hosts map should not be nil")
|
||||
}
|
||||
|
||||
if len(cfg.Hosts) != 1 {
|
||||
t.Errorf("Expected 1 host, got %d", len(cfg.Hosts))
|
||||
}
|
||||
|
||||
host, ok := cfg.Hosts["codeberg.org"]
|
||||
if !ok {
|
||||
t.Fatal("Host codeberg.org not found")
|
||||
}
|
||||
|
||||
if host.Hostname != "codeberg.org" {
|
||||
t.Errorf("Expected hostname 'codeberg.org', got '%s'", host.Hostname)
|
||||
}
|
||||
|
||||
if host.Token != "test-token" {
|
||||
t.Errorf("Expected token 'test-token', got '%s'", host.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_GetHost(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Hosts: map[string]HostConfig{
|
||||
"codeberg.org": {
|
||||
Hostname: "codeberg.org",
|
||||
Token: "test-token",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
host, err := cfg.GetHost("codeberg.org")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if host.Hostname != "codeberg.org" {
|
||||
t.Errorf("Expected hostname 'codeberg.org', got '%s'", host.Hostname)
|
||||
}
|
||||
|
||||
_, err = cfg.GetHost("nonexistent.org")
|
||||
if err == nil {
|
||||
t.Error("Expected error for nonexistent host")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigDir(t *testing.T) {
|
||||
dir, err := GetConfigDir()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if dir == "" {
|
||||
t.Error("Config directory should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigPath(t *testing.T) {
|
||||
path, err := GetConfigPath()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
t.Error("Config path should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_SaveAndLoad(t *testing.T) {
|
||||
// Create a temp directory for testing
|
||||
tempDir := t.TempDir()
|
||||
tempFile := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
// Create a config with test data
|
||||
cfg := &Config{
|
||||
Hosts: map[string]HostConfig{
|
||||
"codeberg.org": {
|
||||
Hostname: "codeberg.org",
|
||||
Token: "test-token-123",
|
||||
User: "testuser",
|
||||
GitProtocol: "ssh",
|
||||
},
|
||||
"github.com": {
|
||||
Hostname: "github.com",
|
||||
Token: "github-token-456",
|
||||
User: "githubuser",
|
||||
GitProtocol: "https",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test Save
|
||||
err := cfg.SaveToPath(tempFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
if _, err := os.Stat(tempFile); os.IsNotExist(err) {
|
||||
t.Fatal("Config file was not created")
|
||||
}
|
||||
|
||||
// Test Load
|
||||
loadedCfg, err := LoadFromPath(tempFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Verify loaded config matches saved config
|
||||
if len(loadedCfg.Hosts) != 2 {
|
||||
t.Errorf("Expected 2 hosts, got %d", len(loadedCfg.Hosts))
|
||||
}
|
||||
|
||||
// Check codeberg.org host
|
||||
codebergHost, ok := loadedCfg.Hosts["codeberg.org"]
|
||||
if !ok {
|
||||
t.Fatal("codeberg.org host not found in loaded config")
|
||||
}
|
||||
if codebergHost.Token != "test-token-123" {
|
||||
t.Errorf("Expected token 'test-token-123', got '%s'", codebergHost.Token)
|
||||
}
|
||||
if codebergHost.User != "testuser" {
|
||||
t.Errorf("Expected user 'testuser', got '%s'", codebergHost.User)
|
||||
}
|
||||
if codebergHost.GitProtocol != "ssh" {
|
||||
t.Errorf("Expected git_protocol 'ssh', got '%s'", codebergHost.GitProtocol)
|
||||
}
|
||||
|
||||
// Check github.com host
|
||||
githubHost, ok := loadedCfg.Hosts["github.com"]
|
||||
if !ok {
|
||||
t.Fatal("github.com host not found in loaded config")
|
||||
}
|
||||
if githubHost.Token != "github-token-456" {
|
||||
t.Errorf("Expected token 'github-token-456', got '%s'", githubHost.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_LoadNonexistentFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
tempFile := filepath.Join(tempDir, "nonexistent.yaml")
|
||||
|
||||
// Test Load with nonexistent file
|
||||
cfg, err := LoadFromPath(tempFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Load should not error on nonexistent file: %v", err)
|
||||
}
|
||||
|
||||
// Should return empty config
|
||||
if cfg == nil {
|
||||
t.Fatal("Config should not be nil")
|
||||
}
|
||||
|
||||
if cfg.Hosts == nil {
|
||||
t.Fatal("Hosts map should be initialized")
|
||||
}
|
||||
|
||||
if len(cfg.Hosts) != 0 {
|
||||
t.Errorf("Expected empty hosts map, got %d entries", len(cfg.Hosts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_LoadInvalidYAML(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
tempFile := filepath.Join(tempDir, "invalid.yaml")
|
||||
|
||||
// Write invalid YAML
|
||||
err := os.WriteFile(tempFile, []byte("invalid: yaml: content: [[["), 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write invalid YAML: %v", err)
|
||||
}
|
||||
|
||||
// Test Load with invalid YAML
|
||||
_, err = LoadFromPath(tempFile)
|
||||
if err == nil {
|
||||
t.Error("Expected error when loading invalid YAML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_SaveCreatesDirectory(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
tempFile := filepath.Join(tempDir, "subdir", "config.yaml")
|
||||
|
||||
cfg := &Config{
|
||||
Hosts: map[string]HostConfig{
|
||||
"test.org": {
|
||||
Hostname: "test.org",
|
||||
Token: "token",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Save should create the directory
|
||||
err := cfg.SaveToPath(tempFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory was created
|
||||
subdir := filepath.Dir(tempFile)
|
||||
if _, err := os.Stat(subdir); os.IsNotExist(err) {
|
||||
t.Error("Save() should have created the directory")
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
if _, err := os.Stat(tempFile); os.IsNotExist(err) {
|
||||
t.Error("Config file was not created")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue