Merge pull request 'feat: basic initial support for actions' (#4) from feat/3-actions into main
Reviewed-on: https://codeberg.org/romaintb/fgj/pulls/4
This commit is contained in:
commit
d94c4072af
4 changed files with 841 additions and 20 deletions
735
cmd/actions.go
Normal file
735
cmd/actions.go
Normal file
|
|
@ -0,0 +1,735 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"codeberg.org/romaintb/fgj/internal/api"
|
||||||
|
"codeberg.org/romaintb/fgj/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionRunList represents a list of workflow runs
|
||||||
|
type ActionRunList struct {
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
WorkflowRuns []ActionRun `json:"workflow_runs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionTaskList represents a list of tasks/jobs
|
||||||
|
type ActionTaskList struct {
|
||||||
|
WorkflowRuns []ActionTask `json:"workflow_runs"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsCmd = &cobra.Command{
|
||||||
|
Use: "actions",
|
||||||
|
Aliases: []string{"action"},
|
||||||
|
Short: "Manage Forgejo Actions",
|
||||||
|
Long: "View and manage workflows, runs, secrets, and variables for Forgejo Actions in your repositories.",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run commands (compatible with gh run)
|
||||||
|
var runCmd = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "View and manage workflow runs",
|
||||||
|
Long: "List, view, and manage workflow runs.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var runListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List recent workflow runs",
|
||||||
|
Long: "List recent workflow runs for a repository.",
|
||||||
|
RunE: runRunList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var runViewCmd = &cobra.Command{
|
||||||
|
Use: "view <run-id>",
|
||||||
|
Short: "View a workflow run",
|
||||||
|
Long: "View details about a specific workflow run.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runRunView,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret commands
|
||||||
|
var actionsSecretCmd = &cobra.Command{
|
||||||
|
Use: "secret",
|
||||||
|
Short: "Manage repository secrets",
|
||||||
|
Long: "List, create, and delete secrets for Forgejo Actions.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsSecretListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List repository secrets",
|
||||||
|
Long: "List all secrets for a repository.",
|
||||||
|
RunE: runActionsSecretList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsSecretCreateCmd = &cobra.Command{
|
||||||
|
Use: "create <name>",
|
||||||
|
Short: "Create or update a repository secret",
|
||||||
|
Long: "Create or update a secret for Forgejo Actions. The secret value will be read from stdin.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runActionsSecretCreate,
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsSecretDeleteCmd = &cobra.Command{
|
||||||
|
Use: "delete <name>",
|
||||||
|
Short: "Delete a repository secret",
|
||||||
|
Long: "Delete a secret from Forgejo Actions.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runActionsSecretDelete,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable commands
|
||||||
|
var actionsVariableCmd = &cobra.Command{
|
||||||
|
Use: "variable",
|
||||||
|
Short: "Manage repository variables",
|
||||||
|
Long: "List, get, create, update, and delete variables for Forgejo Actions.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsVariableListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List repository variables",
|
||||||
|
Long: "List all variables for a repository.",
|
||||||
|
RunE: runActionsVariableList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsVariableGetCmd = &cobra.Command{
|
||||||
|
Use: "get <name>",
|
||||||
|
Short: "Get a repository variable",
|
||||||
|
Long: "Get the value of a specific repository variable.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runActionsVariableGet,
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsVariableCreateCmd = &cobra.Command{
|
||||||
|
Use: "create <name> <value>",
|
||||||
|
Short: "Create a repository variable",
|
||||||
|
Long: "Create a new variable for Forgejo Actions.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: runActionsVariableCreate,
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsVariableUpdateCmd = &cobra.Command{
|
||||||
|
Use: "update <name> <value>",
|
||||||
|
Short: "Update a repository variable",
|
||||||
|
Long: "Update an existing variable for Forgejo Actions.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: runActionsVariableUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsVariableDeleteCmd = &cobra.Command{
|
||||||
|
Use: "delete <name>",
|
||||||
|
Short: "Delete a repository variable",
|
||||||
|
Long: "Delete a variable from Forgejo Actions.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runActionsVariableDelete,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(actionsCmd)
|
||||||
|
|
||||||
|
// Add run commands (gh run compatible)
|
||||||
|
actionsCmd.AddCommand(runCmd)
|
||||||
|
runCmd.AddCommand(runListCmd)
|
||||||
|
runCmd.AddCommand(runViewCmd)
|
||||||
|
|
||||||
|
// Add secret commands
|
||||||
|
actionsCmd.AddCommand(actionsSecretCmd)
|
||||||
|
actionsSecretCmd.AddCommand(actionsSecretListCmd)
|
||||||
|
actionsSecretCmd.AddCommand(actionsSecretCreateCmd)
|
||||||
|
actionsSecretCmd.AddCommand(actionsSecretDeleteCmd)
|
||||||
|
|
||||||
|
// Add variable commands
|
||||||
|
actionsCmd.AddCommand(actionsVariableCmd)
|
||||||
|
actionsVariableCmd.AddCommand(actionsVariableListCmd)
|
||||||
|
actionsVariableCmd.AddCommand(actionsVariableGetCmd)
|
||||||
|
actionsVariableCmd.AddCommand(actionsVariableCreateCmd)
|
||||||
|
actionsVariableCmd.AddCommand(actionsVariableUpdateCmd)
|
||||||
|
actionsVariableCmd.AddCommand(actionsVariableDeleteCmd)
|
||||||
|
|
||||||
|
// Add flags for run commands
|
||||||
|
addRepoFlags(runListCmd)
|
||||||
|
runListCmd.Flags().IntP("limit", "L", 20, "Maximum number of runs to list")
|
||||||
|
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")
|
||||||
|
|
||||||
|
// Add flags for secret commands
|
||||||
|
addRepoFlags(actionsSecretListCmd)
|
||||||
|
addRepoFlags(actionsSecretCreateCmd)
|
||||||
|
addRepoFlags(actionsSecretDeleteCmd)
|
||||||
|
|
||||||
|
// Add flags for variable commands
|
||||||
|
addRepoFlags(actionsVariableListCmd)
|
||||||
|
addRepoFlags(actionsVariableGetCmd)
|
||||||
|
addRepoFlags(actionsVariableCreateCmd)
|
||||||
|
addRepoFlags(actionsVariableUpdateCmd)
|
||||||
|
addRepoFlags(actionsVariableDeleteCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRepoFlags(cmd *cobra.Command) {
|
||||||
|
cmd.Flags().StringP("repo", "R", "", "Repository in owner/name format (auto-detected from git if not specified)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run command implementations
|
||||||
|
|
||||||
|
func runRunList(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, "")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
limit, _ := cmd.Flags().GetInt("limit")
|
||||||
|
|
||||||
|
// Call the API endpoint directly since SDK doesn't have it yet
|
||||||
|
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs?limit=%d", owner, name, limit)
|
||||||
|
|
||||||
|
var runList ActionRunList
|
||||||
|
if err := client.GetJSON(endpoint, &runList); err != nil {
|
||||||
|
return fmt.Errorf("failed to list runs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runList.WorkflowRuns) == 0 {
|
||||||
|
fmt.Println("No workflow runs found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
if _, err := fmt.Fprintln(w, "STATUS\tTITLE\tWORKFLOW\tEVENT\tID\tCREATED"); err != nil {
|
||||||
|
return fmt.Errorf("failed to write header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, run := range runList.WorkflowRuns {
|
||||||
|
createdTime, err := time.Parse(time.RFC3339, run.Created)
|
||||||
|
if err != nil {
|
||||||
|
createdTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
timeStr := formatTimeSince(createdTime)
|
||||||
|
|
||||||
|
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%s\n",
|
||||||
|
formatStatus(run.Status), run.Title, run.WorkflowID, run.Event, run.ID, timeStr); err != nil {
|
||||||
|
return fmt.Errorf("failed to write run: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("failed to flush output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRunView(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, "")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose, _ := cmd.Flags().GetBool("verbose")
|
||||||
|
showLog, _ := cmd.Flags().GetBool("log")
|
||||||
|
jobIDStr, _ := cmd.Flags().GetString("job")
|
||||||
|
showLogFailed, _ := cmd.Flags().GetBool("log-failed")
|
||||||
|
|
||||||
|
var jobID int64
|
||||||
|
if jobIDStr != "" {
|
||||||
|
var err error
|
||||||
|
jobID, err = strconv.ParseInt(jobIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid job ID: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
var run ActionRun
|
||||||
|
if err := client.GetJSON(endpoint, &run); err != nil {
|
||||||
|
return fmt.Errorf("failed to get run: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display run information
|
||||||
|
fmt.Printf("Title: %s\n", run.Title)
|
||||||
|
fmt.Printf("Workflow: %s\n", run.WorkflowID)
|
||||||
|
fmt.Printf("Run: #%d\n", run.IndexInRepo)
|
||||||
|
fmt.Printf("Status: %s\n", formatStatus(run.Status))
|
||||||
|
fmt.Printf("Event: %s\n", run.Event)
|
||||||
|
fmt.Printf("Ref: %s\n", run.PrettyRef)
|
||||||
|
|
||||||
|
commit := run.CommitSHA
|
||||||
|
if len(commit) > 8 {
|
||||||
|
commit = commit[:8]
|
||||||
|
}
|
||||||
|
fmt.Printf("Commit: %s\n", commit)
|
||||||
|
|
||||||
|
if createdTime, err := time.Parse(time.RFC3339, run.Created); err == nil {
|
||||||
|
fmt.Printf("Created: %s\n", createdTime.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
if run.Started != "" {
|
||||||
|
if startedTime, err := time.Parse(time.RFC3339, run.Started); err == nil {
|
||||||
|
fmt.Printf("Started: %s\n", startedTime.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if updatedTime, err := time.Parse(time.RFC3339, run.Updated); err == nil {
|
||||||
|
fmt.Printf("Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch jobs if needed for verbose, log, or job-specific views
|
||||||
|
needsJobs := verbose || showLog || showLogFailed || jobID > 0
|
||||||
|
if !needsJobs {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter tasks for this run number
|
||||||
|
var runTasks []ActionTask
|
||||||
|
for _, task := range taskList.WorkflowRuns {
|
||||||
|
if task.RunNumber == run.IndexInRepo {
|
||||||
|
runTasks = append(runTasks, task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runTasks) == 0 {
|
||||||
|
fmt.Println("\nNo jobs found for this run")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If --job is specified, filter to that job
|
||||||
|
if jobID > 0 {
|
||||||
|
var found bool
|
||||||
|
for _, task := range runTasks {
|
||||||
|
if task.ID == jobID {
|
||||||
|
runTasks = []ActionTask{task}
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("job %d not found in this run", jobID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 1: --verbose (show job steps/details without logs)
|
||||||
|
if verbose && !showLog && !showLogFailed {
|
||||||
|
fmt.Println("\nJobs:")
|
||||||
|
for _, task := range runTasks {
|
||||||
|
fmt.Printf("\n %s - %s (ID: %d)\n", formatStatus(task.Status), task.Name, task.ID)
|
||||||
|
if startedTime, err := time.Parse(time.RFC3339, task.RunStartedAt); err == nil {
|
||||||
|
fmt.Printf(" Started: %s\n", startedTime.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
if updatedTime, err := time.Parse(time.RFC3339, task.UpdatedAt); err == nil {
|
||||||
|
fmt.Printf(" Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
fmt.Printf("\nError fetching log for job %s: %v\n", task.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3: --job without --log or --verbose (show job details only)
|
||||||
|
if jobID > 0 {
|
||||||
|
task := runTasks[0]
|
||||||
|
fmt.Println("\nJob Details:")
|
||||||
|
fmt.Printf(" Name: %s\n", task.Name)
|
||||||
|
fmt.Printf(" ID: %d\n", task.ID)
|
||||||
|
fmt.Printf(" Status: %s\n", formatStatus(task.Status))
|
||||||
|
if startedTime, err := time.Parse(time.RFC3339, task.RunStartedAt); err == nil {
|
||||||
|
fmt.Printf(" Started: %s\n", startedTime.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
if updatedTime, err := time.Parse(time.RFC3339, task.UpdatedAt); err == nil {
|
||||||
|
fmt.Printf(" Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
fmt.Printf("\n========================================\n")
|
||||||
|
fmt.Printf("Job: %s (ID: %d)\n", task.Name, task.ID)
|
||||||
|
fmt.Printf("Status: %s\n", formatStatus(task.Status))
|
||||||
|
fmt.Printf("========================================\n\n")
|
||||||
|
|
||||||
|
// Use GetRawLog helper
|
||||||
|
logContent, err := client.GetRawLog(logURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If --log-failed, filter to only show failed steps
|
||||||
|
// For now, just show all logs (filtering failed steps would require parsing the log format)
|
||||||
|
if logFailed {
|
||||||
|
// TODO: Implement filtering for failed steps only
|
||||||
|
// This would require parsing the log format and identifying failed step markers
|
||||||
|
fmt.Println("Note: --log-failed filtering not yet implemented, showing all logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(logContent)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatStatus(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "success":
|
||||||
|
return "✓ success"
|
||||||
|
case "failure":
|
||||||
|
return "✗ failure"
|
||||||
|
case "cancelled":
|
||||||
|
return "- cancelled"
|
||||||
|
case "skipped":
|
||||||
|
return "○ skipped"
|
||||||
|
case "in_progress", "running":
|
||||||
|
return "● in progress"
|
||||||
|
case "queued", "waiting":
|
||||||
|
return "○ queued"
|
||||||
|
default:
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTimeSince(t time.Time) string {
|
||||||
|
duration := time.Since(t)
|
||||||
|
|
||||||
|
if duration < time.Minute {
|
||||||
|
return "just now"
|
||||||
|
} else if duration < time.Hour {
|
||||||
|
mins := int(duration.Minutes())
|
||||||
|
if mins == 1 {
|
||||||
|
return "1 minute ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d minutes ago", mins)
|
||||||
|
} else if duration < 24*time.Hour {
|
||||||
|
hours := int(duration.Hours())
|
||||||
|
if hours == 1 {
|
||||||
|
return "1 hour ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d hours ago", hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
days := int(duration.Hours() / 24)
|
||||||
|
if days == 1 {
|
||||||
|
return "1 day ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d days ago", days)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret command implementations
|
||||||
|
|
||||||
|
func runActionsSecretList(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, "")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, _, err := client.ListRepoActionSecret(owner, name, gitea.ListRepoActionSecretOption{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list secrets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(secrets) == 0 {
|
||||||
|
fmt.Println("No secrets found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
if _, err := fmt.Fprintln(w, "NAME\tCREATED"); err != nil {
|
||||||
|
return fmt.Errorf("failed to write header: %w", err)
|
||||||
|
}
|
||||||
|
for _, secret := range secrets {
|
||||||
|
if _, err := fmt.Fprintf(w, "%s\t%s\n", secret.Name, secret.Created.Format("2006-01-02 15:04:05")); err != nil {
|
||||||
|
return fmt.Errorf("failed to write secret: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := w.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("failed to flush output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runActionsSecretCreate(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, "")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
secretName := args[0]
|
||||||
|
|
||||||
|
// Read secret value from stdin
|
||||||
|
fmt.Print("Enter secret value: ")
|
||||||
|
var secretValue string
|
||||||
|
_, err = fmt.Scanln(&secretValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read secret value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea.CreateSecretOption{
|
||||||
|
Name: secretName,
|
||||||
|
Data: secretValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.CreateRepoActionSecret(owner, name, opt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Secret '%s' created successfully\n", secretName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runActionsSecretDelete(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, "")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
secretName := args[0]
|
||||||
|
|
||||||
|
_, err = client.DeleteRepoActionSecret(owner, name, secretName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Secret '%s' deleted successfully\n", secretName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable command implementations
|
||||||
|
|
||||||
|
func runActionsVariableList(cmd *cobra.Command, args []string) error {
|
||||||
|
// Note: The SDK doesn't have a ListRepoActionVariable method yet
|
||||||
|
return fmt.Errorf("listing variables is not yet supported in the SDK")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runActionsVariableGet(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, "")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
variableName := args[0]
|
||||||
|
|
||||||
|
variable, _, err := client.GetRepoActionVariable(owner, name, variableName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get variable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s=%s\n", variable.Name, variable.Value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runActionsVariableCreate(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, "")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
variableName := args[0]
|
||||||
|
variableValue := args[1]
|
||||||
|
|
||||||
|
_, err = client.CreateRepoActionVariable(owner, name, variableName, variableValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create variable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Variable '%s' created successfully\n", variableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runActionsVariableUpdate(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, "")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
variableName := args[0]
|
||||||
|
variableValue := args[1]
|
||||||
|
|
||||||
|
_, err = client.UpdateRepoActionVariable(owner, name, variableName, variableValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update variable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Variable '%s' updated successfully\n", variableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runActionsVariableDelete(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, "")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
variableName := args[0]
|
||||||
|
|
||||||
|
_, err = client.DeleteRepoActionVariable(owner, name, variableName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete variable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Variable '%s' deleted successfully\n", variableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
15
go.mod
15
go.mod
|
|
@ -1,20 +1,21 @@
|
||||||
module codeberg.org/romaintb/fgj
|
module codeberg.org/romaintb/fgj
|
||||||
|
|
||||||
go 1.21
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/sdk/gitea v0.18.0
|
code.gitea.io/sdk/gitea v0.22.1
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
golang.org/x/term v0.19.0
|
golang.org/x/term v0.32.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/42wim/httpsig v1.2.3 // indirect
|
||||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
|
@ -29,9 +30,9 @@ require (
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/crypto v0.22.0 // indirect
|
golang.org/x/crypto v0.39.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/sys v0.19.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
29
go.sum
29
go.sum
|
|
@ -1,5 +1,7 @@
|
||||||
code.gitea.io/sdk/gitea v0.18.0 h1:+zZrwVmujIrgobt6wVBWCqITz6bn1aBjnCUHmpZrerI=
|
code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
|
||||||
code.gitea.io/sdk/gitea v0.18.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI=
|
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/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|
@ -15,8 +17,8 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
|
@ -61,8 +63,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
|
@ -72,8 +75,8 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
|
@ -81,15 +84,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
"codeberg.org/romaintb/fgj/internal/config"
|
"codeberg.org/romaintb/fgj/internal/config"
|
||||||
)
|
)
|
||||||
|
|
@ -8,6 +13,7 @@ import (
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*gitea.Client
|
*gitea.Client
|
||||||
hostname string
|
hostname string
|
||||||
|
token string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(hostname, token string) (*Client, error) {
|
func NewClient(hostname, token string) (*Client, error) {
|
||||||
|
|
@ -23,6 +29,7 @@ func NewClient(hostname, token string) (*Client, error) {
|
||||||
return &Client{
|
return &Client{
|
||||||
Client: client,
|
Client: client,
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
|
token: token,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,3 +45,78 @@ func NewClientFromConfig(cfg *config.Config, hostname string) (*Client, error) {
|
||||||
func (c *Client) Hostname() string {
|
func (c *Client) Hostname() string {
|
||||||
return c.hostname
|
return c.hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetJSON performs a GET request to the specified path and decodes the JSON response
|
||||||
|
func (c *Client) GetJSON(path string, result any) error {
|
||||||
|
baseURL := "https://" + c.hostname
|
||||||
|
url := baseURL + path
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set authentication header
|
||||||
|
if c.token != "" {
|
||||||
|
req.Header.Set("Authorization", "token "+c.token)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to perform request: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
|
||||||
|
err = fmt.Errorf("failed to close response body: %w", closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRawLog performs a GET request and returns the raw response body as string
|
||||||
|
func (c *Client) GetRawLog(url string) (string, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set authentication header
|
||||||
|
if c.token != "" {
|
||||||
|
req.Header.Set("Authorization", "token "+c.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to perform request: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
|
||||||
|
err = fmt.Errorf("failed to close response body: %w", closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(bodyBytes), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue