ef8651410d
Block internal-flavored paths / Block forbidden paths (pull_request) Waiting to run
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
E2E API Smoke Test / detect-changes (pull_request) Waiting to run
E2E Chat / detect-changes (pull_request) Waiting to run
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Waiting to run
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Waiting to run
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Waiting to run
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Waiting to run
Handlers Postgres Integration / detect-changes (pull_request) Waiting to run
Harness Replays / detect-changes (pull_request) Waiting to run
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Waiting to run
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Waiting to run
lint-required-no-paths / lint-required-no-paths (pull_request) Waiting to run
Secret scan / Scan diff for credential-shaped strings (pull_request) Waiting to run
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Waiting to run
gate-check-v3 / gate-check (pull_request) Waiting to run
qa-review / approved (pull_request) Waiting to run
security-review / approved (pull_request) Waiting to run
sop-checklist / all-items-acked (pull_request) Waiting to run
sop-checklist / review-refire (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 8s
audit-force-merge / audit (pull_request) Successful in 8s
CI / all-required (pull_request) Failing after 41m6s
CI / Platform (Go) (pull_request) Has been skipped
CI / Canvas (Next.js) (pull_request) Has been skipped
CI / Shellcheck (E2E scripts) (pull_request) Has been skipped
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Detect changes (pull_request) Has been cancelled
CI / Python Lint & Test (pull_request) Has been cancelled
E2E API Smoke Test / E2E API Smoke Test (pull_request) Has been cancelled
E2E Chat / E2E Chat (pull_request) Has been cancelled
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Has been cancelled
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Has been cancelled
Harness Replays / Harness Replays (pull_request) Has been cancelled
177 lines
4.6 KiB
Go
177 lines
4.6 KiB
Go
package templatecache
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type ManifestEntry struct {
|
|
Name string `json:"name"`
|
|
Repo string `json:"repo"`
|
|
Ref string `json:"ref"`
|
|
}
|
|
|
|
type manifestFile struct {
|
|
WorkspaceTemplates []ManifestEntry `json:"workspace_templates"`
|
|
}
|
|
|
|
type TemplateResult struct {
|
|
Name string `json:"name"`
|
|
Repo string `json:"repo"`
|
|
Ref string `json:"ref"`
|
|
SHA string `json:"sha,omitempty"`
|
|
Status string `json:"status"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type RefreshReport struct {
|
|
ManifestPath string `json:"manifest_path"`
|
|
CacheDir string `json:"cache_dir"`
|
|
RefreshedAt time.Time `json:"refreshed_at"`
|
|
Results []TemplateResult `json:"results"`
|
|
}
|
|
|
|
func RefreshWorkspaceTemplates(ctx context.Context, manifestPath, cacheDir, token string) (RefreshReport, error) {
|
|
report := RefreshReport{
|
|
ManifestPath: manifestPath,
|
|
CacheDir: cacheDir,
|
|
RefreshedAt: time.Now().UTC(),
|
|
}
|
|
if strings.TrimSpace(token) == "" {
|
|
return report, fmt.Errorf("template cache refresh requires MOLECULE_TEMPLATE_GITEA_TOKEN or MOLECULE_GITEA_TOKEN")
|
|
}
|
|
data, err := os.ReadFile(manifestPath)
|
|
if err != nil {
|
|
return report, fmt.Errorf("read manifest: %w", err)
|
|
}
|
|
var manifest manifestFile
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
return report, fmt.Errorf("parse manifest: %w", err)
|
|
}
|
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
|
return report, fmt.Errorf("mkdir cache: %w", err)
|
|
}
|
|
for _, entry := range manifest.WorkspaceTemplates {
|
|
result := refreshOne(ctx, cacheDir, token, entry)
|
|
report.Results = append(report.Results, result)
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
func refreshOne(ctx context.Context, cacheDir, token string, entry ManifestEntry) TemplateResult {
|
|
result := TemplateResult{Name: entry.Name, Repo: entry.Repo, Ref: entry.Ref}
|
|
if result.Ref == "" {
|
|
result.Ref = "main"
|
|
}
|
|
if !safeTemplateName(entry.Name) {
|
|
result.Status = "skipped"
|
|
result.Error = "invalid template name"
|
|
return result
|
|
}
|
|
if strings.TrimSpace(entry.Repo) == "" {
|
|
result.Status = "skipped"
|
|
result.Error = "missing repo"
|
|
return result
|
|
}
|
|
|
|
tmp, err := os.MkdirTemp(cacheDir, ".tmp-"+entry.Name+"-")
|
|
if err != nil {
|
|
result.Status = "failed"
|
|
result.Error = err.Error()
|
|
return result
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
|
|
cloneURL := authenticatedURL(entry.Repo, token)
|
|
for _, args := range [][]string{
|
|
{"init", "-q", tmp},
|
|
{"-C", tmp, "remote", "add", "origin", cloneURL},
|
|
{"-C", tmp, "fetch", "--depth=1", "-q", "origin", result.Ref},
|
|
{"-C", tmp, "checkout", "-q", "--detach", "FETCH_HEAD"},
|
|
} {
|
|
cmd := exec.CommandContext(ctx, "git", args...)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
result.Status = "failed"
|
|
result.Error = sanitizeGitError(out, err, token)
|
|
return result
|
|
}
|
|
}
|
|
shaCmd := exec.CommandContext(ctx, "git", "-C", tmp, "rev-parse", "HEAD")
|
|
if out, err := shaCmd.Output(); err == nil {
|
|
result.SHA = strings.TrimSpace(string(out))
|
|
}
|
|
_ = os.RemoveAll(filepath.Join(tmp, ".git"))
|
|
|
|
target := filepath.Join(cacheDir, entry.Name)
|
|
old := filepath.Join(cacheDir, ".old-"+entry.Name+"-"+fmt.Sprint(time.Now().UnixNano()))
|
|
if _, err := os.Stat(target); err == nil {
|
|
if err := os.Rename(target, old); err != nil {
|
|
result.Status = "failed"
|
|
result.Error = "replace old cache: " + err.Error()
|
|
return result
|
|
}
|
|
defer os.RemoveAll(old)
|
|
}
|
|
if err := os.Rename(tmp, target); err != nil {
|
|
if old != "" {
|
|
_ = os.Rename(old, target)
|
|
}
|
|
result.Status = "failed"
|
|
result.Error = "install cache: " + err.Error()
|
|
return result
|
|
}
|
|
result.Status = "refreshed"
|
|
return result
|
|
}
|
|
|
|
func safeTemplateName(name string) bool {
|
|
if name == "" || name == "." || name == ".." {
|
|
return false
|
|
}
|
|
for _, r := range name {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func authenticatedURL(repo, token string) string {
|
|
if strings.HasPrefix(repo, "http://") || strings.HasPrefix(repo, "https://") {
|
|
u, err := url.Parse(repo)
|
|
if err == nil {
|
|
u.User = url.UserPassword("oauth2", token)
|
|
return u.String()
|
|
}
|
|
}
|
|
u := &url.URL{
|
|
Scheme: "https",
|
|
Host: "git.moleculesai.app",
|
|
Path: "/" + strings.TrimSuffix(repo, ".git") + ".git",
|
|
User: url.UserPassword("oauth2", token),
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
func sanitizeGitError(out []byte, err error, token string) string {
|
|
msg := strings.TrimSpace(string(out))
|
|
if msg == "" {
|
|
msg = err.Error()
|
|
}
|
|
if token != "" {
|
|
msg = strings.ReplaceAll(msg, token, "***")
|
|
}
|
|
if len(msg) > 300 {
|
|
msg = msg[:300]
|
|
}
|
|
return msg
|
|
}
|