Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pkg/cmd/run/shared/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ type Job struct {
CompletedAt time.Time `json:"completed_at"`
URL string `json:"html_url"`
RunID int64 `json:"run_id"`

Log *zip.File
}

type Step struct {
Expand All @@ -239,7 +241,8 @@ type Step struct {
Number int
StartedAt time.Time `json:"started_at"`
CompletedAt time.Time `json:"completed_at"`
Log *zip.File

Log *zip.File
}

type Steps []Step
Expand Down
108 changes: 108 additions & 0 deletions pkg/cmd/run/shared/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,60 @@ var SuccessfulJob Job = Job{
},
}

// Note that this run *has* steps, but in the ZIP archive the step logs are not
Comment thread
williammartin marked this conversation as resolved.
// included.
var SuccessfulJobWithoutStepLogs Job = Job{
ID: 11,
Status: Completed,
Conclusion: Success,
Name: "cool job with no step logs",
StartedAt: TestRunStartTime,
CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34),
URL: "https://github.com/jobs/11",
RunID: 3,
Steps: []Step{
{
Name: "fob the barz",
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "barz the fob",
Status: Completed,
Conclusion: Success,
Number: 2,
},
},
}

// Note that this run *has* steps, but in the ZIP archive the step logs are not
// included.
var LegacySuccessfulJobWithoutStepLogs Job = Job{
ID: 12,
Status: Completed,
Conclusion: Success,
Name: "legacy cool job with no step logs",
StartedAt: TestRunStartTime,
CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34),
URL: "https://github.com/jobs/12",
RunID: 3,
Steps: []Step{
{
Name: "fob the barz",
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "barz the fob",
Status: Completed,
Conclusion: Success,
Number: 2,
},
},
}

var FailedJob Job = Job{
ID: 20,
Status: Completed,
Expand All @@ -129,6 +183,60 @@ var FailedJob Job = Job{
},
}

// Note that this run *has* steps, but in the ZIP archive the step logs are not
// included.
var FailedJobWithoutStepLogs Job = Job{
ID: 21,
Status: Completed,
Conclusion: Failure,
Name: "sad job with no step logs",
StartedAt: TestRunStartTime,
CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34),
URL: "https://github.com/jobs/21",
RunID: 1234,
Steps: []Step{
{
Name: "barf the quux",
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "quux the barf",
Status: Completed,
Conclusion: Failure,
Number: 2,
},
},
}

// Note that this run *has* steps, but in the ZIP archive the step logs are not
// included.
var LegacyFailedJobWithoutStepLogs Job = Job{
ID: 22,
Status: Completed,
Conclusion: Failure,
Name: "legacy sad job with no step logs",
StartedAt: TestRunStartTime,
CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34),
URL: "https://github.com/jobs/22",
RunID: 1234,
Steps: []Step{
{
Name: "barf the quux",
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "quux the barf",
Status: Completed,
Conclusion: Failure,
Number: 2,
},
},
}

var SuccessfulJobAnnotations []Annotation = []Annotation{
{
JobName: "cool job",
Expand Down
Binary file modified pkg/cmd/run/view/fixtures/run_log.zip
Binary file not shown.
94 changes: 81 additions & 13 deletions pkg/cmd/run/view/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ func promptForJob(prompter shared.Prompter, cs *iostreams.ColorScheme, jobs []sh

const JOB_NAME_MAX_LENGTH = 90

func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp {
func getJobNameForLogFilename(name string) string {
// As described in https://github.com/cli/cli/issues/5011#issuecomment-1570713070, there are a number of steps
// the server can take when producing the downloaded zip file that can result in a mismatch between the job name
// and the filename in the zip including:
Expand All @@ -545,9 +545,20 @@ func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp {
// * Strip `/` which occur when composite action job names are constructed of the form `<JOB_NAME`> / <ACTION_NAME>`
// * Truncate long job names
//
sanitizedJobName := strings.ReplaceAll(job.Name, "/", "")
sanitizedJobName := strings.ReplaceAll(name, "/", "")
sanitizedJobName = strings.ReplaceAll(sanitizedJobName, ":", "")
sanitizedJobName = truncateAsUTF16(sanitizedJobName, JOB_NAME_MAX_LENGTH)
return sanitizedJobName
}

func jobLogFilenameRegexp(job shared.Job) *regexp.Regexp {
sanitizedJobName := getJobNameForLogFilename(job.Name)
re := fmt.Sprintf(`^-?\d+_%s\.txt`, regexp.QuoteMeta(sanitizedJobName))
return regexp.MustCompile(re)
}

func stepLogFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp {
sanitizedJobName := getJobNameForLogFilename(job.Name)
re := fmt.Sprintf(`^%s\/%d_.*\.txt`, regexp.QuoteMeta(sanitizedJobName), step.Number)
return regexp.MustCompile(re)
}
Expand Down Expand Up @@ -627,17 +638,36 @@ func truncateAsUTF16(str string, max int) string {
// │ ├── 2_anotherstepname.txt
// │ ├── 3_stepstepname.txt
// │ └── 4_laststepname.txt
// └── jobname2/
// ├── 1_stepname.txt
// └── 2_somestepname.txt
// ├── jobname2/
// | ├── 1_stepname.txt
// | └── 2_somestepname.txt
// ├── 0_jobname1.txt
// ├── 1_jobname2.txt
// └── -9999999999_jobname3.txt
//
// It iterates through the list of jobs and tries to find the matching
// log in the zip file. If the matching log is found it is attached
// to the job.
//
// The top-level .txt files include the logs for an entire job run. Note that
// the prefixed number is either:
// - An ordinal and cannot be mapped to the corresponding job's ID.
// - A negative integer which is the ID of the job in the old Actions service.
Comment thread
babakks marked this conversation as resolved.
// The service right now tries to get logs and use an ordinal in a loop.
// However, if it doesn't get the logs, it falls back to an old service
// where the ID can apparently be negative.
func attachRunLog(rlz *zip.Reader, jobs []shared.Job) {
for i, job := range jobs {
re := jobLogFilenameRegexp(job)
for _, file := range rlz.File {
if re.MatchString(file.Name) {
jobs[i].Log = file
break
}
}

for j, step := range job.Steps {
re := logFilenameRegexp(job, step)
re := stepLogFilenameRegexp(job, step)
for _, file := range rlz.File {
if re.MatchString(file.Name) {
jobs[i].Steps[j].Log = file
Expand All @@ -650,6 +680,13 @@ func attachRunLog(rlz *zip.Reader, jobs []shared.Job) {

func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error {
for _, job := range jobs {
// To display a run log, we first try to compile it from individual step
// logs, because this way we can prepend lines with the corresponding
// step name. However, at the time of writing, logs are sometimes being
// served by a service that doesn’t include the step logs (none of them),
// in which case we fall back to print the entire job run log.
var hasStepLogs bool
Comment thread
williammartin marked this conversation as resolved.

steps := job.Steps
sort.Sort(steps)
for _, step := range steps {
Expand All @@ -659,18 +696,49 @@ func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error {
if step.Log == nil {
continue
}
hasStepLogs = true
prefix := fmt.Sprintf("%s\t%s\t", job.Name, step.Name)
f, err := step.Log.Open()
if err != nil {
if err := printZIPFile(w, step.Log, prefix); err != nil {
return err
}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Fprintf(w, "%s%s\n", prefix, scanner.Text())
}
f.Close()
}

if hasStepLogs {
continue
}

if failed && !shared.IsFailureState(job.Conclusion) {
continue
}

if job.Log == nil {
continue
}

// Here, we fall back to the job run log, which means we do not know
Comment thread
babakks marked this conversation as resolved.
// the step name of lines. However, we want to keep the same line
// formatting to avoid breaking any code or script that rely on the
// tab-delimited formatting. So, an unknown-step placeholder is used
// instead of the actual step name.
prefix := fmt.Sprintf("%s\tUNKNOWN STEP\t", job.Name)
if err := printZIPFile(w, job.Log, prefix); err != nil {
return err
}
}

return nil
}

func printZIPFile(w io.Writer, file *zip.File, prefix string) error {
f, err := file.Open()
if err != nil {
return err
}
defer f.Close()

scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Fprintf(w, "%s%s\n", prefix, scanner.Text())
}
return nil
}
Loading
Loading