// tinqs/ci orchestrator — executor Lambda // // Handles deploy-only jobs directly in Lambda (no Fargate needed). // Parses workflow YAML, runs shell steps, reports commit status to Gitea. // 15 min timeout, 2 GB memory, 5 GB /tmp. package main import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "os" "os/exec" "time" "github.com/aws/aws-lambda-go/lambda" "gopkg.in/yaml.v3" ) type ExecPayload struct { Repo GiteaRepo `json:"repo"` Ref string `json:"ref"` CommitSHA string `json:"commit_sha"` Pusher string `json:"pusher"` Workflow string `json:"workflow_name"` WorkflowYAML string `json:"workflow_yaml"` GiteaURL string `json:"gitea_url"` GiteaToken string `json:"gitea_token"` } type GiteaRepo struct { FullName string `json:"full_name"` CloneURL string `json:"clone_url"` } type Workflow struct { Name string `yaml:"name"` Jobs map[string]WorkflowJob `yaml:"jobs"` } type WorkflowJob struct { Steps []WorkflowStep `yaml:"steps"` Env map[string]string `yaml:"env"` } type WorkflowStep struct { Name string `yaml:"name"` Run string `yaml:"run"` Uses string `yaml:"uses"` Env map[string]string `yaml:"env"` } type StepResult struct { Name string `json:"name"` Status string `json:"status"` Duration string `json:"duration"` } type JobResult struct { Workflow string `json:"workflow"` Status string `json:"status"` Steps []StepResult `json:"steps"` Duration string `json:"duration"` } func handler(ctx context.Context, payload ExecPayload) (JobResult, error) { sha := payload.CommitSHA if len(sha) > 7 { sha = sha[:7] } log.Printf("Executor: repo=%s workflow=%s sha=%s", payload.Repo.FullName, payload.Workflow, sha) start := time.Now() var wf Workflow if err := yaml.Unmarshal([]byte(payload.WorkflowYAML), &wf); err != nil { return JobResult{Workflow: payload.Workflow, Status: "failure"}, err } setCommitStatus(payload, "pending", "CI running...") overallStatus := "success" var steps []StepResult for _, job := range wf.Jobs { // Build environment env := os.Environ() env = append(env, "CI=true", fmt.Sprintf("GITHUB_REPOSITORY=%s", payload.Repo.FullName), fmt.Sprintf("GITHUB_REF=%s", payload.Ref), fmt.Sprintf("GITHUB_SHA=%s", payload.CommitSHA), ) for k, v := range job.Env { env = append(env, fmt.Sprintf("%s=%s", k, v)) } workdir := "/tmp/workspace" os.MkdirAll(workdir, 0755) os.RemoveAll(workdir) os.MkdirAll(workdir, 0755) for i, step := range job.Steps { stepStart := time.Now() name := step.Name if name == "" { name = fmt.Sprintf("Step %d", i+1) } // Handle checkout action if step.Uses != "" && contains(step.Uses, "checkout") { cloneURL := payload.Repo.CloneURL if payload.GiteaToken != "" { cloneURL = fmt.Sprintf("https://token:%s@%s", payload.GiteaToken, cloneURL[8:]) // strip https:// } branch := payload.Ref if len(branch) > 11 && branch[:11] == "refs/heads/" { branch = branch[11:] } cmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", "--branch", branch, cloneURL, workdir) if out, err := cmd.CombinedOutput(); err != nil { log.Printf("Checkout failed: %s", string(out)) steps = append(steps, StepResult{Name: name, Status: "failure", Duration: since(stepStart)}) overallStatus = "failure" break } steps = append(steps, StepResult{Name: name, Status: "success", Duration: since(stepStart)}) continue } // Skip unknown actions if step.Uses != "" { steps = append(steps, StepResult{Name: name, Status: "skipped", Duration: since(stepStart)}) continue } if step.Run == "" { continue } // Run shell step stepEnv := make([]string, len(env)) copy(stepEnv, env) for k, v := range step.Env { stepEnv = append(stepEnv, fmt.Sprintf("%s=%s", k, v)) } cmd := exec.CommandContext(ctx, "bash", "-c", step.Run) cmd.Dir = workdir cmd.Env = stepEnv var output bytes.Buffer cmd.Stdout = io.MultiWriter(&output, os.Stdout) cmd.Stderr = io.MultiWriter(&output, os.Stderr) if err := cmd.Run(); err != nil { log.Printf("Step %d FAILED: %v", i+1, err) steps = append(steps, StepResult{Name: name, Status: "failure", Duration: since(stepStart)}) overallStatus = "failure" break } steps = append(steps, StepResult{Name: name, Status: "success", Duration: since(stepStart)}) } if overallStatus == "failure" { break } } desc := fmt.Sprintf("CI %s in %s", overallStatus, time.Since(start).Round(time.Second)) setCommitStatus(payload, overallStatus, desc) return JobResult{ Workflow: payload.Workflow, Status: overallStatus, Steps: steps, Duration: time.Since(start).Round(time.Second).String(), }, nil } func setCommitStatus(payload ExecPayload, state, description string) { if payload.GiteaURL == "" || payload.GiteaToken == "" { return } url := fmt.Sprintf("%s/api/v1/repos/%s/statuses/%s", payload.GiteaURL, payload.Repo.FullName, payload.CommitSHA) body, _ := json.Marshal(map[string]string{ "state": state, "description": description, "context": fmt.Sprintf("ci/%s", payload.Workflow), }) req, _ := http.NewRequest("POST", url, bytes.NewReader(body)) req.Header.Set("Authorization", "token "+payload.GiteaToken) req.Header.Set("Content-Type", "application/json") resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req) if err != nil { log.Printf("Failed to set commit status: %v", err) return } resp.Body.Close() } func contains(s, sub string) bool { return len(s) >= len(sub) && (s == sub || len(s) > 0 && containsStr(s, sub)) } func containsStr(s, sub string) bool { for i := 0; i <= len(s)-len(sub); i++ { if s[i:i+len(sub)] == sub { return true } } return false } func since(t time.Time) string { return time.Since(t).Round(time.Millisecond).String() } func main() { lambda.Start(handler) }