Merge pull request 'feat/various' (#28) from feat/various into main
Reviewed-on: https://codeberg.org/romaintb/fgj/pulls/28
This commit is contained in:
commit
65c686997f
14 changed files with 683 additions and 103 deletions
479
cmd/actions.go
479
cmd/actions.go
|
|
@ -2,8 +2,10 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
|
|
@ -16,17 +18,17 @@ import (
|
|||
|
||||
// ActionRun represents a workflow run
|
||||
type ActionRun struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
IndexInRepo int64 `json:"index_in_repo"`
|
||||
Event string `json:"event"`
|
||||
Status string `json:"status"`
|
||||
CommitSHA string `json:"commit_sha"`
|
||||
PrettyRef string `json:"prettyref"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Started string `json:"started"`
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
IndexInRepo int64 `json:"index_in_repo"`
|
||||
Event string `json:"event"`
|
||||
Status string `json:"status"`
|
||||
CommitSHA string `json:"commit_sha"`
|
||||
PrettyRef string `json:"prettyref"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Started string `json:"started"`
|
||||
}
|
||||
|
||||
// ActionRunList represents a list of workflow runs
|
||||
|
|
@ -37,19 +39,19 @@ type ActionRunList struct {
|
|||
|
||||
// ActionTask represents a job/task within a workflow run
|
||||
type ActionTask struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
HeadBranch string `json:"head_branch"`
|
||||
HeadSHA string `json:"head_sha"`
|
||||
RunNumber int64 `json:"run_number"`
|
||||
Event string `json:"event"`
|
||||
DisplayTitle string `json:"display_title"`
|
||||
Status string `json:"status"`
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
URL string `json:"url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
RunStartedAt string `json:"run_started_at"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
HeadBranch string `json:"head_branch"`
|
||||
HeadSHA string `json:"head_sha"`
|
||||
RunNumber int64 `json:"run_number"`
|
||||
Event string `json:"event"`
|
||||
DisplayTitle string `json:"display_title"`
|
||||
Status string `json:"status"`
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
URL string `json:"url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
RunStartedAt string `json:"run_started_at"`
|
||||
}
|
||||
|
||||
// ActionTaskList represents a list of tasks/jobs
|
||||
|
|
@ -109,6 +111,30 @@ var runViewCmd = &cobra.Command{
|
|||
RunE: runRunView,
|
||||
}
|
||||
|
||||
var runWatchCmd = &cobra.Command{
|
||||
Use: "watch <run-id>",
|
||||
Short: "Watch a workflow run",
|
||||
Long: "Poll a workflow run until it completes.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRunWatch,
|
||||
}
|
||||
|
||||
var runRerunCmd = &cobra.Command{
|
||||
Use: "rerun <run-id>",
|
||||
Short: "Rerun a workflow run",
|
||||
Long: "Trigger a rerun for a specific workflow run.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRunRerun,
|
||||
}
|
||||
|
||||
var runCancelCmd = &cobra.Command{
|
||||
Use: "cancel <run-id>",
|
||||
Short: "Cancel a workflow run",
|
||||
Long: "Cancel a running workflow run.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRunCancel,
|
||||
}
|
||||
|
||||
// Workflow commands
|
||||
var workflowCmd = &cobra.Command{
|
||||
Use: "workflow",
|
||||
|
|
@ -139,6 +165,22 @@ var workflowRunCmd = &cobra.Command{
|
|||
RunE: runWorkflowRun,
|
||||
}
|
||||
|
||||
var workflowEnableCmd = &cobra.Command{
|
||||
Use: "enable <workflow>",
|
||||
Short: "Enable a workflow",
|
||||
Long: "Enable a workflow so it can be triggered.\n\nNote: This feature requires Forgejo 15.0+ or Gitea 1.24+.\nFor older versions, use the web UI to enable workflows.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runWorkflowEnable,
|
||||
}
|
||||
|
||||
var workflowDisableCmd = &cobra.Command{
|
||||
Use: "disable <workflow>",
|
||||
Short: "Disable a workflow",
|
||||
Long: "Disable a workflow so it cannot be triggered.\n\nNote: This feature requires Forgejo 15.0+ or Gitea 1.24+.\nFor older versions, use the web UI to disable workflows.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runWorkflowDisable,
|
||||
}
|
||||
|
||||
// Secret commands
|
||||
var actionsSecretCmd = &cobra.Command{
|
||||
Use: "secret",
|
||||
|
|
@ -222,12 +264,17 @@ func init() {
|
|||
actionsCmd.AddCommand(runCmd)
|
||||
runCmd.AddCommand(runListCmd)
|
||||
runCmd.AddCommand(runViewCmd)
|
||||
runCmd.AddCommand(runWatchCmd)
|
||||
runCmd.AddCommand(runRerunCmd)
|
||||
runCmd.AddCommand(runCancelCmd)
|
||||
|
||||
// Add workflow commands (gh workflow compatible)
|
||||
actionsCmd.AddCommand(workflowCmd)
|
||||
workflowCmd.AddCommand(workflowListCmd)
|
||||
workflowCmd.AddCommand(workflowViewCmd)
|
||||
workflowCmd.AddCommand(workflowRunCmd)
|
||||
workflowCmd.AddCommand(workflowEnableCmd)
|
||||
workflowCmd.AddCommand(workflowDisableCmd)
|
||||
|
||||
// Add secret commands
|
||||
actionsCmd.AddCommand(actionsSecretCmd)
|
||||
|
|
@ -246,17 +293,27 @@ func init() {
|
|||
// Add flags for run commands
|
||||
addRepoFlags(runListCmd)
|
||||
runListCmd.Flags().IntP("limit", "L", 20, "Maximum number of runs to list")
|
||||
runListCmd.Flags().Bool("json", false, "Output workflow runs as JSON")
|
||||
addRepoFlags(runViewCmd)
|
||||
runViewCmd.Flags().BoolP("verbose", "v", false, "Show job steps")
|
||||
runViewCmd.Flags().BoolP("log", "", false, "View full log for either a run or specific job")
|
||||
runViewCmd.Flags().StringP("job", "j", "", "View a specific job ID from a run")
|
||||
runViewCmd.Flags().BoolP("log-failed", "", false, "View the log for any failed steps in a run or specific job")
|
||||
runViewCmd.Flags().Bool("json", false, "Output workflow run as JSON")
|
||||
addRepoFlags(runWatchCmd)
|
||||
runWatchCmd.Flags().DurationP("interval", "i", 5*time.Second, "Polling interval")
|
||||
addRepoFlags(runRerunCmd)
|
||||
addRepoFlags(runCancelCmd)
|
||||
|
||||
// Add flags for workflow commands
|
||||
addRepoFlags(workflowListCmd)
|
||||
workflowListCmd.Flags().IntP("limit", "L", 20, "Maximum number of workflows to list")
|
||||
workflowListCmd.Flags().Bool("json", false, "Output workflows as JSON")
|
||||
addRepoFlags(workflowViewCmd)
|
||||
workflowViewCmd.Flags().Bool("json", false, "Output workflow as JSON")
|
||||
addRepoFlags(workflowRunCmd)
|
||||
addRepoFlags(workflowEnableCmd)
|
||||
addRepoFlags(workflowDisableCmd)
|
||||
workflowRunCmd.Flags().StringP("ref", "r", "", "Branch or tag name to run the workflow on (defaults to repository's default branch)")
|
||||
workflowRunCmd.Flags().StringSliceP("field", "f", nil, "Add a string parameter in key=value format (can be used multiple times)")
|
||||
workflowRunCmd.Flags().StringSliceP("raw-field", "F", nil, "Add a string parameter in key=value format, reading from file if value starts with @ (can be used multiple times)")
|
||||
|
|
@ -307,6 +364,10 @@ func runRunList(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("failed to list runs: %w", err)
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
return writeJSON(runList.WorkflowRuns)
|
||||
}
|
||||
|
||||
if len(runList.WorkflowRuns) == 0 {
|
||||
fmt.Println("No workflow runs found")
|
||||
return nil
|
||||
|
|
@ -364,6 +425,7 @@ func runRunView(cmd *cobra.Command, args []string) error {
|
|||
showLog, _ := cmd.Flags().GetBool("log")
|
||||
jobIDStr, _ := cmd.Flags().GetString("job")
|
||||
showLogFailed, _ := cmd.Flags().GetBool("log-failed")
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
var jobID int64
|
||||
if jobIDStr != "" {
|
||||
|
|
@ -374,6 +436,10 @@ func runRunView(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
if jsonOutput && (showLog || showLogFailed) {
|
||||
return fmt.Errorf("--json cannot be used with --log or --log-failed")
|
||||
}
|
||||
|
||||
// Call the API endpoint directly since SDK doesn't have it yet
|
||||
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", owner, name, runID)
|
||||
|
||||
|
|
@ -382,6 +448,48 @@ func runRunView(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("failed to get run: %w", err)
|
||||
}
|
||||
|
||||
needsJobs := verbose || showLog || showLogFailed || jobID > 0
|
||||
|
||||
if jsonOutput {
|
||||
var runTasks []ActionTask
|
||||
if needsJobs {
|
||||
tasksEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", owner, name)
|
||||
var taskList ActionTaskList
|
||||
if err := client.GetJSON(tasksEndpoint, &taskList); err != nil {
|
||||
return fmt.Errorf("failed to get tasks: %w", err)
|
||||
}
|
||||
|
||||
for _, task := range taskList.WorkflowRuns {
|
||||
if task.RunNumber == run.IndexInRepo {
|
||||
runTasks = append(runTasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
if jobID > 0 {
|
||||
var filtered []ActionTask
|
||||
for _, task := range runTasks {
|
||||
if task.ID == jobID {
|
||||
filtered = append(filtered, task)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return fmt.Errorf("job %d not found in this run", jobID)
|
||||
}
|
||||
runTasks = filtered
|
||||
}
|
||||
}
|
||||
|
||||
payload := struct {
|
||||
Run ActionRun `json:"run"`
|
||||
Tasks []ActionTask `json:"tasks,omitempty"`
|
||||
}{
|
||||
Run: run,
|
||||
Tasks: runTasks,
|
||||
}
|
||||
return writeJSON(payload)
|
||||
}
|
||||
|
||||
// Display run information
|
||||
fmt.Printf("Title: %s\n", run.Title)
|
||||
fmt.Printf("Workflow: %s\n", run.WorkflowID)
|
||||
|
|
@ -409,7 +517,6 @@ func runRunView(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
// Fetch jobs if needed for verbose, log, or job-specific views
|
||||
needsJobs := verbose || showLog || showLogFailed || jobID > 0
|
||||
if !needsJobs {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -466,7 +573,7 @@ func runRunView(cmd *cobra.Command, args []string) error {
|
|||
// Case 2: --log or --log-failed (show logs)
|
||||
if showLog || showLogFailed {
|
||||
for _, task := range runTasks {
|
||||
if err := showJobLog(client, owner, name, run.IndexInRepo, task, showLogFailed); err != nil {
|
||||
if err := showJobLog(client, owner, name, task, showLogFailed); err != nil {
|
||||
fmt.Printf("\nError fetching log for job %s: %v\n", task.Name, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -491,10 +598,122 @@ func runRunView(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func showJobLog(client *api.Client, owner, name string, runNumber int64, task ActionTask, logFailed bool) error {
|
||||
// Fetch log from /repos/{owner}/{repo}/actions/runs/{run_number}/jobs/{job_id}/logs
|
||||
logURL := fmt.Sprintf("https://%s/%s/%s/actions/runs/%d/jobs/%d/logs",
|
||||
client.Hostname(), owner, name, runNumber, task.ID)
|
||||
func runRunWatch(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
repo, _ := cmd.Flags().GetString("repo")
|
||||
owner, name, err := parseRepo(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runID, err := strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid run ID: %w", err)
|
||||
}
|
||||
|
||||
interval, _ := cmd.Flags().GetDuration("interval")
|
||||
if interval <= 0 {
|
||||
return fmt.Errorf("interval must be greater than 0")
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", owner, name, runID)
|
||||
|
||||
var lastStatus string
|
||||
for {
|
||||
var run ActionRun
|
||||
if err := client.GetJSON(endpoint, &run); err != nil {
|
||||
return fmt.Errorf("failed to get run: %w", err)
|
||||
}
|
||||
|
||||
if run.Status != lastStatus {
|
||||
fmt.Printf("Status: %s\n", formatStatus(run.Status))
|
||||
lastStatus = run.Status
|
||||
}
|
||||
|
||||
if isRunComplete(run.Status) {
|
||||
fmt.Printf("Run #%d completed with status %s\n", run.IndexInRepo, formatStatus(run.Status))
|
||||
return nil
|
||||
}
|
||||
|
||||
time.Sleep(interval)
|
||||
}
|
||||
}
|
||||
|
||||
func runRunRerun(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
repo, _ := cmd.Flags().GetString("repo")
|
||||
owner, name, err := parseRepo(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runID, err := strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid run ID: %w", err)
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/rerun", owner, name, runID)
|
||||
if err := client.PostJSON(endpoint, nil, nil); err != nil {
|
||||
return fmt.Errorf("failed to rerun workflow: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Rerun requested for run %d\n", runID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRunCancel(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
repo, _ := cmd.Flags().GetString("repo")
|
||||
owner, name, err := parseRepo(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runID, err := strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid run ID: %w", err)
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/cancel", owner, name, runID)
|
||||
if err := client.PostJSON(endpoint, nil, nil); err != nil {
|
||||
return fmt.Errorf("failed to cancel workflow run: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Cancel requested for run %d\n", runID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func showJobLog(client *api.Client, owner, name string, task ActionTask, logFailed bool) error {
|
||||
// Fetch log from API: GET /api/v1/repos/{owner}/{repo}/actions/jobs/{job_id}/logs
|
||||
logURL := fmt.Sprintf("https://%s/api/v1/repos/%s/%s/actions/jobs/%d/logs",
|
||||
client.Hostname(), owner, name, task.ID)
|
||||
|
||||
fmt.Printf("\n========================================\n")
|
||||
fmt.Printf("Job: %s (ID: %d)\n", task.Name, task.ID)
|
||||
|
|
@ -540,6 +759,15 @@ func formatStatus(status string) string {
|
|||
}
|
||||
}
|
||||
|
||||
func isRunComplete(status string) bool {
|
||||
switch status {
|
||||
case "success", "failure", "cancelled", "skipped":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func formatTimeSince(t time.Time) string {
|
||||
duration := time.Since(t)
|
||||
|
||||
|
|
@ -620,10 +848,17 @@ func runWorkflowList(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
if len(allWorkflows) == 0 {
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
return writeJSON(allWorkflows)
|
||||
}
|
||||
fmt.Println("No workflows found")
|
||||
return nil
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
return writeJSON(allWorkflows)
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
if _, err := fmt.Fprintln(w, "NAME\tSTATE\tPATH"); err != nil {
|
||||
return fmt.Errorf("failed to write header: %w", err)
|
||||
|
|
@ -661,37 +896,31 @@ func runWorkflowView(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
workflowIdentifier := args[0]
|
||||
|
||||
// Find the workflow by listing from both .gitea/workflows and .forgejo/workflows
|
||||
var workflow *Workflow
|
||||
|
||||
for _, dir := range []string{".gitea/workflows", ".forgejo/workflows"} {
|
||||
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, name, dir)
|
||||
|
||||
var contents []ContentsResponse
|
||||
if err := client.GetJSON(endpoint, &contents); err != nil {
|
||||
// Directory might not exist, continue
|
||||
continue
|
||||
}
|
||||
|
||||
for _, content := range contents {
|
||||
if content.Type == "file" && (content.Name == workflowIdentifier || content.Path == workflowIdentifier) {
|
||||
workflow = &Workflow{
|
||||
Name: content.Name,
|
||||
Path: content.Path,
|
||||
State: "active",
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if workflow != nil {
|
||||
break
|
||||
}
|
||||
workflow, err := findWorkflow(client, owner, name, workflowIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if workflow == nil {
|
||||
return fmt.Errorf("workflow '%s' not found", workflowIdentifier)
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
var latestRun *ActionRun
|
||||
|
||||
// Get the latest run for this workflow
|
||||
runsEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs?workflow_id=%s&limit=1", owner, name, workflow.Path)
|
||||
var runList ActionRunList
|
||||
if err := client.GetJSON(runsEndpoint, &runList); err == nil && len(runList.WorkflowRuns) > 0 {
|
||||
latestRun = &runList.WorkflowRuns[0]
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
payload := struct {
|
||||
Workflow *Workflow `json:"workflow"`
|
||||
LatestRun *ActionRun `json:"latest_run,omitempty"`
|
||||
}{
|
||||
Workflow: workflow,
|
||||
LatestRun: latestRun,
|
||||
}
|
||||
return writeJSON(payload)
|
||||
}
|
||||
|
||||
// Display workflow information
|
||||
|
|
@ -699,21 +928,12 @@ func runWorkflowView(cmd *cobra.Command, args []string) error {
|
|||
fmt.Printf("Path: %s\n", workflow.Path)
|
||||
fmt.Printf("State: %s\n", workflow.State)
|
||||
|
||||
// Get the latest run for this workflow
|
||||
runsEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs?workflow_id=%s&limit=1", owner, name, workflow.Path)
|
||||
var runList ActionRunList
|
||||
if err := client.GetJSON(runsEndpoint, &runList); err != nil {
|
||||
// If we can't get runs, just display workflow info without latest run
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(runList.WorkflowRuns) > 0 {
|
||||
run := runList.WorkflowRuns[0]
|
||||
if latestRun != nil {
|
||||
fmt.Printf("\nLatest run:\n")
|
||||
fmt.Printf(" Status: %s\n", formatStatus(run.Status))
|
||||
fmt.Printf(" Event: %s\n", run.Event)
|
||||
fmt.Printf(" Ref: %s\n", run.PrettyRef)
|
||||
if createdTime, err := time.Parse(time.RFC3339, run.Created); err == nil {
|
||||
fmt.Printf(" Status: %s\n", formatStatus(latestRun.Status))
|
||||
fmt.Printf(" Event: %s\n", latestRun.Event)
|
||||
fmt.Printf(" Ref: %s\n", latestRun.PrettyRef)
|
||||
if createdTime, err := time.Parse(time.RFC3339, latestRun.Created); err == nil {
|
||||
fmt.Printf(" Created: %s\n", formatTimeSince(createdTime))
|
||||
}
|
||||
}
|
||||
|
|
@ -798,6 +1018,96 @@ func runWorkflowRun(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runWorkflowEnable(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
repo, _ := cmd.Flags().GetString("repo")
|
||||
owner, name, err := parseRepo(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
workflowIdentifier := args[0]
|
||||
workflow, err := findWorkflow(client, owner, name, workflowIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/enable", owner, name, workflow.Name)
|
||||
|
||||
// Try PUT first (correct method per GitHub/Gitea API spec)
|
||||
status, err := client.DoJSON(http.MethodPut, endpoint, nil, nil)
|
||||
if err != nil && (status == http.StatusNotFound || status == http.StatusMethodNotAllowed) {
|
||||
// Fall back to POST for older versions
|
||||
status, err = client.DoJSON(http.MethodPost, endpoint, nil, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if status == http.StatusNotFound && strings.Contains(err.Error(), "404") {
|
||||
return fmt.Errorf("failed to enable workflow: this feature requires Forgejo 15.0+ or Gitea 1.24+. " +
|
||||
"Your instance does not support the workflow enable/disable API endpoints yet. " +
|
||||
"You can enable workflows via the web UI instead")
|
||||
}
|
||||
return fmt.Errorf("failed to enable workflow: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Workflow '%s' enabled\n", workflow.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runWorkflowDisable(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
repo, _ := cmd.Flags().GetString("repo")
|
||||
owner, name, err := parseRepo(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
workflowIdentifier := args[0]
|
||||
workflow, err := findWorkflow(client, owner, name, workflowIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/disable", owner, name, workflow.Name)
|
||||
|
||||
// Try PUT first (correct method per GitHub/Gitea API spec)
|
||||
status, err := client.DoJSON(http.MethodPut, endpoint, nil, nil)
|
||||
if err != nil && (status == http.StatusNotFound || status == http.StatusMethodNotAllowed) {
|
||||
// Fall back to POST for older versions
|
||||
status, err = client.DoJSON(http.MethodPost, endpoint, nil, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if status == http.StatusNotFound && strings.Contains(err.Error(), "404") {
|
||||
return fmt.Errorf("failed to disable workflow: this feature requires Forgejo 15.0+ or Gitea 1.24+. " +
|
||||
"Your instance does not support the workflow enable/disable API endpoints yet. " +
|
||||
"You can disable workflows via the web UI instead")
|
||||
}
|
||||
return fmt.Errorf("failed to disable workflow: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Workflow '%s' disabled\n", workflow.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitKeyValue(s string) []string {
|
||||
idx := -1
|
||||
for i, c := range s {
|
||||
|
|
@ -812,6 +1122,29 @@ func splitKeyValue(s string) []string {
|
|||
return []string{s[:idx], s[idx+1:]}
|
||||
}
|
||||
|
||||
func findWorkflow(client *api.Client, owner, name, workflowIdentifier string) (*Workflow, error) {
|
||||
for _, dir := range []string{".gitea/workflows", ".forgejo/workflows"} {
|
||||
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, name, dir)
|
||||
|
||||
var contents []ContentsResponse
|
||||
if err := client.GetJSON(endpoint, &contents); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, content := range contents {
|
||||
if content.Type == "file" && (content.Name == workflowIdentifier || content.Path == workflowIdentifier) {
|
||||
return &Workflow{
|
||||
Name: content.Name,
|
||||
Path: content.Path,
|
||||
State: "active",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("workflow '%s' not found", workflowIdentifier)
|
||||
}
|
||||
|
||||
// Secret command implementations
|
||||
|
||||
func runActionsSecretList(cmd *cobra.Command, args []string) error {
|
||||
|
|
|
|||
90
cmd/auth.go
90
cmd/auth.go
|
|
@ -7,9 +7,10 @@ import (
|
|||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"codeberg.org/romaintb/fgj/internal/api"
|
||||
"codeberg.org/romaintb/fgj/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
|
|
@ -33,13 +34,31 @@ var authStatusCmd = &cobra.Command{
|
|||
RunE: runAuthStatus,
|
||||
}
|
||||
|
||||
var authLogoutCmd = &cobra.Command{
|
||||
Use: "logout",
|
||||
Short: "Remove authentication for a Forgejo instance",
|
||||
Long: "Remove authentication for a configured Forgejo instance.",
|
||||
RunE: runAuthLogout,
|
||||
}
|
||||
|
||||
var authTokenCmd = &cobra.Command{
|
||||
Use: "token",
|
||||
Short: "Print the stored authentication token",
|
||||
Long: "Print the stored authentication token for a configured Forgejo instance.",
|
||||
RunE: runAuthToken,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(authCmd)
|
||||
authCmd.AddCommand(authLoginCmd)
|
||||
authCmd.AddCommand(authStatusCmd)
|
||||
authCmd.AddCommand(authLogoutCmd)
|
||||
authCmd.AddCommand(authTokenCmd)
|
||||
|
||||
authLoginCmd.Flags().String("hostname", "", "Forgejo instance hostname (e.g., codeberg.org)")
|
||||
authLoginCmd.Flags().StringP("token", "t", "", "Personal access token")
|
||||
authLogoutCmd.Flags().String("hostname", "", "Forgejo instance hostname (e.g., codeberg.org)")
|
||||
authTokenCmd.Flags().String("hostname", "", "Forgejo instance hostname (e.g., codeberg.org)")
|
||||
}
|
||||
|
||||
func runAuthLogin(cmd *cobra.Command, args []string) error {
|
||||
|
|
@ -87,9 +106,9 @@ func runAuthLogin(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
cfg.SetHost(hostname, config.HostConfig{
|
||||
Hostname: hostname,
|
||||
Token: token,
|
||||
User: user.UserName,
|
||||
Hostname: hostname,
|
||||
Token: token,
|
||||
User: user.UserName,
|
||||
GitProtocol: "https",
|
||||
})
|
||||
|
||||
|
|
@ -121,3 +140,66 @@ func runAuthStatus(cmd *cobra.Command, args []string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAuthLogout(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
hostname, _ := cmd.Flags().GetString("hostname")
|
||||
resolved, err := resolveAuthHostname(cfg, hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(cfg.Hosts, resolved)
|
||||
if err := cfg.Save(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Logged out from %s\n", resolved)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAuthToken(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
hostname, _ := cmd.Flags().GetString("hostname")
|
||||
resolved, err := resolveAuthHostname(cfg, hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(cfg.Hosts[resolved].Token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveAuthHostname(cfg *config.Config, hostname string) (string, error) {
|
||||
if hostname == "" {
|
||||
hostname = viper.GetString("hostname")
|
||||
}
|
||||
if hostname == "" {
|
||||
hostname = os.Getenv("FGJ_HOST")
|
||||
}
|
||||
if hostname == "" {
|
||||
hostname = getDetectedHost()
|
||||
}
|
||||
if hostname == "" && len(cfg.Hosts) == 1 {
|
||||
for host := range cfg.Hosts {
|
||||
hostname = host
|
||||
}
|
||||
}
|
||||
if hostname == "" {
|
||||
hostname = "codeberg.org"
|
||||
}
|
||||
|
||||
if _, ok := cfg.Hosts[hostname]; !ok {
|
||||
return "", fmt.Errorf("no configuration found for host %s", hostname)
|
||||
}
|
||||
|
||||
return hostname, nil
|
||||
}
|
||||
|
|
|
|||
37
cmd/completion.go
Normal file
37
cmd/completion.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var completionCmd = &cobra.Command{
|
||||
Use: "completion [bash|zsh|fish|powershell]",
|
||||
Short: "Generate shell completion scripts",
|
||||
Long: "Generate shell completion scripts for fgj.",
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
||||
DisableFlagsInUseLine: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var out io.Writer = os.Stdout
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
return rootCmd.GenBashCompletion(out)
|
||||
case "zsh":
|
||||
return rootCmd.GenZshCompletion(out)
|
||||
case "fish":
|
||||
return rootCmd.GenFishCompletion(out, true)
|
||||
case "powershell":
|
||||
return rootCmd.GenPowerShellCompletionWithDesc(out)
|
||||
default:
|
||||
return fmt.Errorf("unsupported shell: %s", args[0])
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(completionCmd)
|
||||
}
|
||||
41
cmd/issue.go
41
cmd/issue.go
|
|
@ -76,8 +76,10 @@ func init() {
|
|||
|
||||
issueListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||
issueListCmd.Flags().StringP("state", "s", "open", "Filter by state: open, closed, all")
|
||||
issueListCmd.Flags().Bool("json", false, "Output issues as JSON")
|
||||
|
||||
issueViewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||
issueViewCmd.Flags().Bool("json", false, "Output issue as JSON")
|
||||
|
||||
issueCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||
issueCreateCmd.Flags().StringP("title", "t", "", "Title for the issue")
|
||||
|
|
@ -133,17 +135,26 @@ func runIssueList(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("failed to list issues: %w", err)
|
||||
}
|
||||
|
||||
if len(issues) == 0 {
|
||||
nonPRIssues := make([]*gitea.Issue, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.PullRequest == nil {
|
||||
nonPRIssues = append(nonPRIssues, issue)
|
||||
}
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
return writeJSON(nonPRIssues)
|
||||
}
|
||||
|
||||
if len(nonPRIssues) == 0 {
|
||||
fmt.Printf("No %s issues in %s/%s\n", state, owner, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = 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)
|
||||
}
|
||||
for _, issue := range nonPRIssues {
|
||||
_, _ = fmt.Fprintf(w, "#%d\t%s\t%s\n", issue.Index, issue.Title, issue.State)
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
|
|
@ -177,6 +188,23 @@ func runIssueView(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("failed to get issue: %w", err)
|
||||
}
|
||||
|
||||
var comments []*gitea.Comment
|
||||
comments, _, err = client.ListIssueComments(owner, name, issueNumber, gitea.ListIssueCommentOptions{})
|
||||
if err != nil {
|
||||
comments = nil
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
payload := struct {
|
||||
Issue *gitea.Issue `json:"issue"`
|
||||
Comments []*gitea.Comment `json:"comments,omitempty"`
|
||||
}{
|
||||
Issue: issue,
|
||||
Comments: comments,
|
||||
}
|
||||
return writeJSON(payload)
|
||||
}
|
||||
|
||||
fmt.Printf("Issue #%d\n", issue.Index)
|
||||
fmt.Printf("Title: %s\n", issue.Title)
|
||||
fmt.Printf("State: %s\n", issue.State)
|
||||
|
|
@ -187,8 +215,7 @@ func runIssueView(cmd *cobra.Command, args []string) error {
|
|||
fmt.Printf("\n%s\n", issue.Body)
|
||||
}
|
||||
|
||||
comments, _, err := client.ListIssueComments(owner, name, issueNumber, gitea.ListIssueCommentOptions{})
|
||||
if err == nil && len(comments) > 0 {
|
||||
if len(comments) > 0 {
|
||||
fmt.Printf("\nComments (%d):\n", len(comments))
|
||||
for _, comment := range comments {
|
||||
fmt.Printf("\n---\n%s (@%s) - %s\n%s\n",
|
||||
|
|
|
|||
12
cmd/json.go
Normal file
12
cmd/json.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
func writeJSON(value any) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(value)
|
||||
}
|
||||
49
cmd/manpages.go
Normal file
49
cmd/manpages.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
var manpagesCmd = &cobra.Command{
|
||||
Use: "manpages",
|
||||
Short: "Generate manpages",
|
||||
Long: "Generate manpages for fgj commands.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dir, _ := cmd.Flags().GetString("dir")
|
||||
if dir == "" {
|
||||
return fmt.Errorf("directory is required")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", dir, err)
|
||||
}
|
||||
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve %s: %w", dir, err)
|
||||
}
|
||||
|
||||
header := &doc.GenManHeader{
|
||||
Title: "FGJ",
|
||||
Section: "1",
|
||||
}
|
||||
|
||||
if err := doc.GenManTree(rootCmd, header, absDir); err != nil {
|
||||
return fmt.Errorf("failed to generate manpages: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Manpages generated in %s\n", absDir)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(manpagesCmd)
|
||||
manpagesCmd.Flags().String("dir", "", "Output directory for manpages")
|
||||
_ = manpagesCmd.MarkFlagRequired("dir")
|
||||
}
|
||||
13
cmd/pr.go
13
cmd/pr.go
|
|
@ -8,9 +8,9 @@ import (
|
|||
"text/tabwriter"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/spf13/cobra"
|
||||
"codeberg.org/romaintb/fgj/internal/api"
|
||||
"codeberg.org/romaintb/fgj/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var prCmd = &cobra.Command{
|
||||
|
|
@ -59,8 +59,10 @@ func init() {
|
|||
|
||||
prListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||
prListCmd.Flags().StringP("state", "s", "open", "Filter by state: open, closed, all")
|
||||
prListCmd.Flags().Bool("json", false, "Output pull requests as JSON")
|
||||
|
||||
prViewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||
prViewCmd.Flags().Bool("json", false, "Output pull request as JSON")
|
||||
|
||||
prCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||
prCreateCmd.Flags().StringP("title", "t", "", "Title for the pull request")
|
||||
|
|
@ -111,6 +113,10 @@ func runPRList(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("failed to list pull requests: %w", err)
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
return writeJSON(prs)
|
||||
}
|
||||
|
||||
if len(prs) == 0 {
|
||||
fmt.Printf("No %s pull requests in %s/%s\n", state, owner, name)
|
||||
return nil
|
||||
|
|
@ -153,6 +159,10 @@ func runPRView(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("failed to get pull request: %w", err)
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
return writeJSON(pr)
|
||||
}
|
||||
|
||||
fmt.Printf("Pull Request #%d\n", pr.Index)
|
||||
fmt.Printf("Title: %s\n", pr.Title)
|
||||
fmt.Printf("State: %s\n", pr.State)
|
||||
|
|
@ -279,4 +289,3 @@ func runPRMerge(cmd *cobra.Command, args []string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,8 +72,10 @@ func init() {
|
|||
releaseListCmd.Flags().Bool("draft", false, "Filter by draft status")
|
||||
releaseListCmd.Flags().Bool("prerelease", false, "Filter by prerelease status")
|
||||
releaseListCmd.Flags().Int("limit", 30, "Maximum number of releases to fetch")
|
||||
releaseListCmd.Flags().Bool("json", false, "Output releases as JSON")
|
||||
|
||||
releaseViewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||
releaseViewCmd.Flags().Bool("json", false, "Output release as JSON")
|
||||
|
||||
releaseCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||
releaseCreateCmd.Flags().StringP("title", "t", "", "Release title (defaults to tag)")
|
||||
|
|
@ -146,6 +148,10 @@ func runReleaseList(cmd *cobra.Command, args []string) error {
|
|||
releases = releases[:limit]
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
return writeJSON(releases)
|
||||
}
|
||||
|
||||
if len(releases) == 0 {
|
||||
fmt.Printf("No releases in %s/%s\n", owner, name)
|
||||
return nil
|
||||
|
|
@ -186,6 +192,22 @@ func runReleaseView(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
attachments, err := listReleaseAttachments(client, owner, name, release.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
payload := struct {
|
||||
Release *gitea.Release `json:"release"`
|
||||
Assets []*gitea.Attachment `json:"assets,omitempty"`
|
||||
}{
|
||||
Release: release,
|
||||
Assets: attachments,
|
||||
}
|
||||
return writeJSON(payload)
|
||||
}
|
||||
|
||||
fmt.Printf("Release %s\n", release.TagName)
|
||||
fmt.Printf("Title: %s\n", release.Title)
|
||||
fmt.Printf("Type: %s\n", releaseType(release))
|
||||
|
|
@ -206,10 +228,6 @@ func runReleaseView(cmd *cobra.Command, args []string) error {
|
|||
fmt.Printf("\n%s\n", release.Note)
|
||||
}
|
||||
|
||||
attachments, err := listReleaseAttachments(client, owner, name, release.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(attachments) > 0 {
|
||||
fmt.Printf("\nAssets (%d):\n", len(attachments))
|
||||
for _, asset := range attachments {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import (
|
|||
"text/tabwriter"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/spf13/cobra"
|
||||
"codeberg.org/romaintb/fgj/internal/api"
|
||||
"codeberg.org/romaintb/fgj/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var repoCmd = &cobra.Command{
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/romaintb/fgj/internal/git"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"codeberg.org/romaintb/fgj/internal/git"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -12,6 +12,7 @@ require (
|
|||
|
||||
require (
|
||||
github.com/42wim/httpsig v1.2.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
|
|
@ -21,6 +22,7 @@ require (
|
|||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -2,6 +2,7 @@ code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
|
|||
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -38,6 +39,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
|
|
|
|||
|
|
@ -88,6 +88,13 @@ func (c *Client) GetJSON(path string, result any) error {
|
|||
|
||||
// PostJSON performs a POST request to the specified path with JSON body
|
||||
func (c *Client) PostJSON(path string, body any, result any) error {
|
||||
_, err := c.DoJSON(http.MethodPost, path, body, result)
|
||||
return err
|
||||
}
|
||||
|
||||
// DoJSON performs an HTTP request with a JSON body and decodes the JSON response.
|
||||
// Returns the HTTP status code and any error encountered.
|
||||
func (c *Client) DoJSON(method string, path string, body any, result any) (int, error) {
|
||||
baseURL := "https://" + c.hostname
|
||||
url := baseURL + path
|
||||
|
||||
|
|
@ -95,14 +102,14 @@ func (c *Client) PostJSON(path string, body any, result any) error {
|
|||
if body != nil {
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request body: %w", err)
|
||||
return 0, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(bodyBytes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, bodyReader)
|
||||
req, err := http.NewRequest(method, url, bodyReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
return 0, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set authentication header
|
||||
|
|
@ -110,12 +117,14 @@ func (c *Client) PostJSON(path string, body any, result any) error {
|
|||
req.Header.Set("Authorization", "token "+c.token)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
httpClient := &http.Client{}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to perform request: %w", err)
|
||||
return 0, fmt.Errorf("failed to perform request: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
|
||||
|
|
@ -125,16 +134,16 @@ func (c *Client) PostJSON(path string, body any, result any) error {
|
|||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
return resp.StatusCode, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
if result != nil && resp.StatusCode != http.StatusNoContent {
|
||||
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
||||
return fmt.Errorf("failed to decode response: %w", err)
|
||||
return resp.StatusCode, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// GetRawLog performs a GET request and returns the raw response body as string
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ type Config struct {
|
|||
}
|
||||
|
||||
type HostConfig struct {
|
||||
Hostname string `yaml:"hostname"`
|
||||
Token string `yaml:"token"`
|
||||
User string `yaml:"user,omitempty"`
|
||||
Hostname string `yaml:"hostname"`
|
||||
Token string `yaml:"token"`
|
||||
User string `yaml:"user,omitempty"`
|
||||
GitProtocol string `yaml:"git_protocol,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue