Files
tasks/tools/flakereport/github.go
zeekay 40351cce6f rip: delete temporal fork — native ZAP-only tasksd
Goes from 135 MB binary with 1153 files importing go.temporal.io and 224
importing google.golang.org/grpc to a 10 MB native binary with ZERO of
either.

Deleted:
- tasks/, tasktests/, service/, chasm/, client/, api/, proto/, tools/
- cmd/tools/, components/, docker/
- schema/{cassandra,elasticsearch,mysql,postgresql,sqlite}/
- 40 tainted common/ subdirs + 7 top-level common files
- tests/testutils (no longer referenced after surviving deps drop common)

New cmd/tasksd (~100 lines) boots tasks.Embed (zap.Node) + http.Server.
New pkg/tasks/embed.go registers ZAP handlers for the SDK opcode surface
(0x0060-0x00A5). Workflow handlers return a 501 "not yet implemented"
envelope; native engine lands behind this contract.

Build:  go build ./cmd/tasksd → 10 MB
Vet:    go vet ./... → clean
Boot:   tasksd --zap-port 9999 --http :7243
Probes: /healthz, /v1/tasks/health → 200
UI:     /_/tasks/, / → 200 (embedded Vite bundle)
ZAP:    _tasks._tcp on :9999 (mDNS disabled; direct connect)

go.mod: tidy. No go.temporal.io. No google.golang.org/grpc.
2026-04-26 14:06:21 -07:00

293 lines
8.7 KiB
Go

package flakereport
import (
"archive/zip"
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// fetchWorkflowRuns retrieves all completed workflow runs within a date range.
// since is the oldest bound (inclusive); until is the newest bound (zero means open-ended).
// Implements proper pagination to fix the 100-run limit bug.
func fetchWorkflowRuns(ctx context.Context, repo string, workflowID int64, branch string, since, until time.Time) ([]WorkflowRun, error) {
var allRuns []WorkflowRun
createdFilter := ">=" + since.Format("2006-01-02")
if !until.IsZero() {
createdFilter = since.Format("2006-01-02") + ".." + until.Format("2006-01-02")
}
fmt.Printf("Fetching workflow runs created %s...\n", createdFilter)
page := 1
for {
ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
cmd := exec.CommandContext(ctxTimeout, "gh", "api",
fmt.Sprintf("/repos/%s/actions/workflows/%d/runs?branch=%s&created=%s&per_page=100&page=%d",
repo, workflowID, branch, createdFilter, page),
)
output, err := cmd.Output()
cancel() // Cancel context immediately after command completes
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("gh api failed (page %d): %w\nstderr: %s", page, err, string(exitErr.Stderr))
}
return nil, fmt.Errorf("failed to execute gh command (page %d): %w", page, err)
}
var response struct {
WorkflowRuns []WorkflowRun `json:"workflow_runs"`
}
if err := json.Unmarshal(output, &response); err != nil {
return nil, fmt.Errorf("failed to parse workflow runs response (page %d): %w", page, err)
}
if len(response.WorkflowRuns) == 0 {
break
}
allRuns = append(allRuns, response.WorkflowRuns...)
fmt.Printf("Fetched page %d: %d runs (total: %d)\n", page, len(response.WorkflowRuns), len(allRuns))
// If we got fewer than 100 results, this is the last page
if len(response.WorkflowRuns) < 100 {
break
}
page++
}
fmt.Printf("Total workflow runs fetched: %d\n", len(allRuns))
return allRuns, nil
}
// fetchRunArtifacts retrieves all artifacts for a specific workflow run
func fetchRunArtifacts(ctx context.Context, repo string, runID int64) ([]WorkflowArtifact, error) {
ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctxTimeout, "gh", "api",
fmt.Sprintf("/repos/%s/actions/runs/%d/artifacts?per_page=100", repo, runID),
)
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("gh api failed for run %d: %w\nstderr: %s", runID, err, string(exitErr.Stderr))
}
return nil, fmt.Errorf("failed to execute gh command for run %d: %w", runID, err)
}
var response ArtifactsResponse
if err := json.Unmarshal(output, &response); err != nil {
return nil, fmt.Errorf("failed to parse artifacts response for run %d: %w", runID, err)
}
// Filter for JUnit/test artifacts
var testArtifacts []WorkflowArtifact
for _, artifact := range response.Artifacts {
if artifact.Expired {
continue
}
name := strings.ToLower(artifact.Name)
if strings.Contains(name, "junit") {
testArtifacts = append(testArtifacts, artifact)
}
}
return testArtifacts, nil
}
// downloadArtifact downloads a single artifact zip file
func downloadArtifact(ctx context.Context, repo string, artifactID int64, outputDir string) (string, error) {
ctxTimeout, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
zipPath := filepath.Join(outputDir, fmt.Sprintf("artifact-%d.zip", artifactID))
cmd := exec.CommandContext(ctxTimeout, "gh", "api",
fmt.Sprintf("/repos/%s/actions/artifacts/%d/zip", repo, artifactID),
)
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("failed to download artifact %d: %w\nstderr: %s", artifactID, err, string(exitErr.Stderr))
}
return "", fmt.Errorf("failed to download artifact %d: %w", artifactID, err)
}
if err := os.WriteFile(zipPath, output, 0644); err != nil {
return "", fmt.Errorf("failed to write artifact zip %d: %w", artifactID, err)
}
return zipPath, nil
}
// extractArtifactZip extracts zip file and returns paths to JUnit XML files
func extractArtifactZip(zipPath, outputDir string) ([]string, error) {
r, err := zip.OpenReader(zipPath)
if err != nil {
return nil, fmt.Errorf("failed to open zip file %s: %w", zipPath, err)
}
defer func() {
if err := r.Close(); err != nil {
fmt.Printf("Warning: Failed to close zip reader: %v\n", err)
}
}()
var xmlFiles []string
for _, f := range r.File {
// Skip directories
if f.FileInfo().IsDir() {
continue
}
// Only extract XML files
if !strings.HasSuffix(strings.ToLower(f.Name), ".xml") {
continue
}
// Create extraction path
extractPath := filepath.Join(outputDir, filepath.Base(f.Name))
// Open file from zip
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file %s in zip: %w", f.Name, err)
}
// Create output file
outFile, err := os.Create(extractPath)
if err != nil {
_ = rc.Close()
return nil, fmt.Errorf("failed to create output file %s: %w", extractPath, err)
}
// Copy content
_, err = io.Copy(outFile, rc)
if closeErr := rc.Close(); closeErr != nil {
_ = outFile.Close()
return nil, fmt.Errorf("failed to close zip file reader: %w", closeErr)
}
if closeErr := outFile.Close(); closeErr != nil {
return nil, fmt.Errorf("failed to close output file: %w", closeErr)
}
if err != nil {
return nil, fmt.Errorf("failed to extract file %s: %w", f.Name, err)
}
xmlFiles = append(xmlFiles, extractPath)
}
return xmlFiles, nil
}
// parseArtifactName extracts run_id, job_id, and matrix_name from artifact name.
// Functional tests: junit-xml--{run_id}--{job_id}--{run_attempt}--{matrix_name}--{display_name}--functional-test
// Unit/integration: junit-xml--{run_id}--{job_id}--{run_attempt}--unit-test
// Returns: runID, jobID, matrixName ("unknown" for fields that are absent or unparseable)
func parseArtifactName(artifactName string) (runID string, jobID string, matrixName string) {
parts := strings.Split(artifactName, "--")
if len(parts) < 3 {
return "unknown", "unknown", "unknown"
}
runID = parts[1]
jobID = parts[2]
if jobID == "" {
jobID = "unknown"
}
// Functional test artifacts carry a matrix name (DB config) at parts[4].
// Unit/integration artifacts have only 5 parts where parts[4] is the test type
// (e.g. "unit-test"), not a matrix name. Functional artifacts have >=7 parts.
if len(parts) >= 6 {
matrixName = parts[4]
} else {
matrixName = "unknown"
}
return runID, jobID, matrixName
}
// buildGitHubURL constructs GitHub Actions URL from run/job IDs
// If jobID == "unknown": https://github.com/{repo}/actions/runs/{runID}
// Otherwise: https://github.com/{repo}/actions/runs/{runID}/job/{jobID}
func buildGitHubURL(repo, runID, jobID string) string {
baseURL := fmt.Sprintf("https://github.com/%s/actions/runs/%s", repo, runID)
if jobID != "unknown" && jobID != "" {
return fmt.Sprintf("%s/job/%s", baseURL, jobID)
}
return baseURL
}
// fetchCommitMeta fetches commit title, author, and changed file list for a single commit SHA.
// Uses: GET /repos/{owner}/{repo}/commits/{sha}
func fetchCommitMeta(ctx context.Context, repo, sha string) (CommitMeta, error) {
ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctxTimeout, "gh", "api",
fmt.Sprintf("/repos/%s/commits/%s", repo, sha),
)
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return CommitMeta{SHA: sha}, fmt.Errorf("gh api failed for commit %s: %w\nstderr: %s", sha, err, string(exitErr.Stderr))
}
return CommitMeta{SHA: sha}, fmt.Errorf("failed to execute gh command for commit %s: %w", sha, err)
}
var response struct {
SHA string `json:"sha"`
Commit struct {
Message string `json:"message"`
Author struct {
Name string `json:"name"`
Date time.Time `json:"date"`
} `json:"author"`
} `json:"commit"`
Files []struct {
Filename string `json:"filename"`
} `json:"files"`
}
if err := json.Unmarshal(output, &response); err != nil {
return CommitMeta{SHA: sha}, fmt.Errorf("failed to parse commit response for %s: %w", sha, err)
}
// Extract just the first line of the commit message as the title
title := response.Commit.Message
if idx := strings.IndexByte(title, '\n'); idx >= 0 {
title = title[:idx]
}
files := make([]string, 0, len(response.Files))
for _, f := range response.Files {
files = append(files, f.Filename)
}
return CommitMeta{
SHA: sha,
Title: title,
Author: response.Commit.Author.Name,
CommittedAt: response.Commit.Author.Date,
Files: files,
}, nil
}