diff --git a/.github/workflows/prauto.yml b/.github/workflows/prauto.yml index 08d41a99f..20a88b31e 100644 --- a/.github/workflows/prauto.yml +++ b/.github/workflows/prauto.yml @@ -1,6 +1,6 @@ name: PR Automation on: - pull_request: + pull_request_target: types: [ready_for_review, opened, reopened] jobs: pr-auto: diff --git a/pkg/cmd/actions/actions.go b/pkg/cmd/actions/actions.go index f0e3509a4..5be2df305 100644 --- a/pkg/cmd/actions/actions.go +++ b/pkg/cmd/actions/actions.go @@ -21,7 +21,7 @@ func NewCmdActions(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "actions", Short: "Learn about working with GitHub actions", - Args: cobra.ExactArgs(0), + Long: actionsExplainer(nil), Hidden: true, Run: func(cmd *cobra.Command, args []string) { actionsRun(opts) @@ -34,28 +34,45 @@ func NewCmdActions(f *cmdutil.Factory) *cobra.Command { return cmd } +func actionsExplainer(cs *iostreams.ColorScheme) string { + header := "Welcome to GitHub Actions on the command line." + runHeader := "Interacting with workflow runs" + workflowHeader := "Interacting with workflow files" + if cs != nil { + header = cs.Bold(header) + runHeader = cs.Bold(runHeader) + workflowHeader = cs.Bold(workflowHeader) + } + + return heredoc.Docf(` + %s + + gh integrates with Actions to help you manage runs and workflows. + + %s + gh run list: List recent workflow runs + gh run view: View details for a workflow run or one of its jobs + gh run watch: Watch a workflow run while it executes + gh run rerun: Rerun a failed workflow run + gh run download: Download artifacts generated by runs + + To see more help, run 'gh help run ' + + %s + gh workflow list: List all the workflow files in your repository + gh workflow view: View details for a workflow file + gh workflow enable: Enable a workflow file + gh workflow disable: Disable a workflow file + gh workflow run: Trigger a workflow_dispatch run for a workflow file + + To see more help, run 'gh help workflow ' + + For more in depth help including examples, see online documentation at: + https://docs.github.com/en/actions/guides/managing-github-actions-with-github-cli + `, header, runHeader, workflowHeader) +} + func actionsRun(opts ActionsOptions) { cs := opts.IO.ColorScheme() - fmt.Fprint(opts.IO.Out, heredoc.Docf(` - Welcome to GitHub Actions on the command line. - - This part of gh is in beta and subject to change! - - To follow along while we get to GA, please see this - tracking issue: https://github.com/cli/cli/issues/2889 - - %s - gh run list: List recent workflow runs - gh run view: View details for a workflow run or one of its jobs - gh run watch: Watch a workflow run while it executes - gh run rerun: Rerun a failed workflow run - - %s - gh workflow list: List all the workflow files in your repository - gh workflow enable: Enable a workflow file - gh workflow disable: Disable a workflow file - gh workflow run: Trigger a workflow_dispatch run for a workflow file - `, - cs.Bold("Interacting with workflow runs"), - cs.Bold("Interacting with workflow files"))) + fmt.Fprintln(opts.IO.Out, actionsExplainer(cs)) } diff --git a/pkg/cmd/run/download/zip.go b/pkg/cmd/run/download/zip.go index e3959a091..d7a2613bc 100644 --- a/pkg/cmd/run/download/zip.go +++ b/pkg/cmd/run/download/zip.go @@ -16,9 +16,13 @@ const ( ) func extractZip(zr *zip.Reader, destDir string) error { - pathPrefix := filepath.Clean(destDir) + string(filepath.Separator) + destDirAbs, err := filepath.Abs(destDir) + if err != nil { + return err + } + pathPrefix := destDirAbs + string(filepath.Separator) for _, zf := range zr.File { - fpath := filepath.Join(destDir, filepath.FromSlash(zf.Name)) + fpath := filepath.Join(destDirAbs, filepath.FromSlash(zf.Name)) if !strings.HasPrefix(fpath, pathPrefix) { continue } diff --git a/pkg/cmd/run/download/zip_test.go b/pkg/cmd/run/download/zip_test.go new file mode 100644 index 000000000..f859511ee --- /dev/null +++ b/pkg/cmd/run/download/zip_test.go @@ -0,0 +1,32 @@ +package download + +import ( + "archive/zip" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_extractZip(t *testing.T) { + tmpDir := t.TempDir() + wd, err := os.Getwd() + require.NoError(t, err) + t.Cleanup(func() { _ = os.Chdir(wd) }) + + zipFile, err := zip.OpenReader("./fixtures/myproject.zip") + require.NoError(t, err) + defer zipFile.Close() + + extractPath := filepath.Join(tmpDir, "artifact") + err = os.MkdirAll(extractPath, 0700) + require.NoError(t, err) + require.NoError(t, os.Chdir(extractPath)) + + err = extractZip(&zipFile.Reader, ".") + require.NoError(t, err) + + _, err = os.Stat(filepath.Join("src", "main.go")) + require.NoError(t, err) +} diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index 90a16071d..9ca0c48f8 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -257,6 +257,7 @@ func GetJobs(client *api.Client, repo ghrepo.Interface, run Run) ([]Job, error) func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) { var selected int + now := time.Now() candidates := []string{} @@ -264,7 +265,7 @@ func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) { symbol, _ := Symbol(cs, run.Status, run.Conclusion) candidates = append(candidates, // TODO truncate commit message, long ones look terrible - fmt.Sprintf("%s %s, %s (%s)", symbol, run.CommitMsg(), run.Name, run.HeadBranch)) + fmt.Sprintf("%s %s, %s (%s) %s", symbol, run.CommitMsg(), run.Name, run.HeadBranch, preciseAgo(now, run.CreatedAt))) } // TODO consider custom filter so it's fuzzier. right now matches start anywhere in string but @@ -380,3 +381,14 @@ func PullRequestForRun(client *api.Client, repo ghrepo.Interface, run Run) (int, return number, nil } + +func preciseAgo(now time.Time, createdAt time.Time) string { + ago := now.Sub(createdAt) + + if ago < 30*24*time.Hour { + s := ago.Truncate(time.Second).String() + return fmt.Sprintf("%s ago", s) + } + + return createdAt.Format("Jan _2, 2006") +} diff --git a/pkg/cmd/run/shared/shared_test.go b/pkg/cmd/run/shared/shared_test.go new file mode 100644 index 000000000..bd6e73962 --- /dev/null +++ b/pkg/cmd/run/shared/shared_test.go @@ -0,0 +1,31 @@ +package shared + +import ( + "testing" + "time" +) + +func TestPreciseAgo(t *testing.T) { + const form = "2006-Jan-02 15:04:05" + now, _ := time.Parse(form, "2021-Apr-12 14:00:00") + + cases := map[string]string{ + "2021-Apr-12 14:00:00": "0s ago", + "2021-Apr-12 13:59:30": "30s ago", + "2021-Apr-12 13:59:00": "1m0s ago", + "2021-Apr-12 13:30:15": "29m45s ago", + "2021-Apr-12 13:00:00": "1h0m0s ago", + "2021-Apr-12 02:30:45": "11h29m15s ago", + "2021-Apr-11 14:00:00": "24h0m0s ago", + "2021-Apr-01 14:00:00": "264h0m0s ago", + "2021-Mar-12 14:00:00": "Mar 12, 2021", + } + + for createdAt, expected := range cases { + d, _ := time.Parse(form, createdAt) + got := preciseAgo(now, d) + if got != expected { + t.Errorf("expected %s but got %s for %s", expected, got, createdAt) + } + } +} diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index d4d0db1f6..9751d1f38 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -105,7 +105,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman $ gh run view --log --job 456789 # Exit non-zero if a run failed - $ gh run view 0451 -e && echo "run pending or passed" + $ gh run view 0451 --exit-status && echo "run pending or passed" `), RunE: func(cmd *cobra.Command, args []string) error { // support `-R, --repo` override @@ -256,7 +256,7 @@ func runView(opts *ViewOptions) error { } opts.IO.StartProgressIndicator() - runLogZip, err := getRunLog(opts.RunLogCache, httpClient, repo, run.ID) + runLogZip, err := getRunLog(opts.RunLogCache, httpClient, repo, run) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to get run log: %w", err) @@ -408,13 +408,13 @@ func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) { return resp.Body, nil } -func getRunLog(cache runLogCache, httpClient *http.Client, repo ghrepo.Interface, runID int) (*zip.ReadCloser, error) { - filename := fmt.Sprintf("run-log-%d.zip", runID) +func getRunLog(cache runLogCache, httpClient *http.Client, repo ghrepo.Interface, run *shared.Run) (*zip.ReadCloser, error) { + filename := fmt.Sprintf("run-log-%d-%d.zip", run.ID, run.CreatedAt.Unix()) filepath := filepath.Join(os.TempDir(), "gh-cli-cache", filename) if !cache.Exists(filepath) { // Run log does not exist in cache so retrieve and store it logURL := fmt.Sprintf("%srepos/%s/actions/runs/%d/logs", - ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), runID) + ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), run.ID) resp, err := getLog(httpClient, logURL) if err != nil { @@ -498,6 +498,9 @@ func displayRunLog(io *iostreams.IOStreams, jobs []shared.Job, failed bool) erro if failed && !shared.IsFailureState(step.Conclusion) { continue } + if step.Log == nil { + continue + } prefix := fmt.Sprintf("%s\t%s\t", job.Name, step.Name) f, err := step.Log.Open() if err != nil {