217 lines
5.9 KiB
Go
217 lines
5.9 KiB
Go
// Dagger pipeline for multi-platform builds.
|
|
// Usage:
|
|
//
|
|
// go run ci/dagger.go [--release v1.0.0]
|
|
//
|
|
// If --release is provided, uploads all built binaries in dist/ to the specified Gitea release.
|
|
// Requires .env file with GITEA_TOKEN, GITEA_URL, GITEA_OWNER, GITEA_REPO.
|
|
// Requires: go install dagger.io/dagger@latest && go get github.com/joho/godotenv && go mod tidy
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"dagger.io/dagger"
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
func main() {
|
|
releaseTag := flag.String("release", "", "(optional) Release tag to upload binaries to Gitea")
|
|
flag.Parse()
|
|
|
|
ctx := context.Background()
|
|
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer client.Close()
|
|
|
|
platforms := []struct {
|
|
OS string
|
|
Arch string
|
|
}{
|
|
{"darwin", "arm64"},
|
|
{"darwin", "amd64"},
|
|
{"windows", "amd64"},
|
|
{"linux", "amd64"},
|
|
}
|
|
|
|
src := client.Host().Directory(".")
|
|
|
|
var builtBinaries []string
|
|
|
|
for _, p := range platforms {
|
|
binName := fmt.Sprintf("valhallir-deconvolver-%s-%s", p.OS, p.Arch)
|
|
if p.OS == "windows" {
|
|
binName += ".exe"
|
|
}
|
|
|
|
ctr := client.Container().From("golang:1.24.5").
|
|
WithMountedDirectory("/src", src).
|
|
WithWorkdir("/src").
|
|
WithEnvVariable("GOOS", p.OS).
|
|
WithEnvVariable("GOARCH", p.Arch).
|
|
WithExec([]string{"go", "build", "-o", binName, "."})
|
|
|
|
// Export the binary from the container to the host's dist/ directory
|
|
outPath := fmt.Sprintf("dist/%s", binName)
|
|
_, err := ctr.File(binName).Export(ctx, outPath)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("export failed for %s/%s: %v", p.OS, p.Arch, err))
|
|
}
|
|
fmt.Printf("Built and exported %s\n", outPath)
|
|
builtBinaries = append(builtBinaries, outPath)
|
|
}
|
|
|
|
if *releaseTag != "" {
|
|
fmt.Printf("\nUploading binaries to Gitea release: %s\n", *releaseTag)
|
|
// Load .env
|
|
err := godotenv.Load("ci/.env")
|
|
if err != nil {
|
|
panic("Error loading .env file: " + err.Error())
|
|
}
|
|
giteaToken := os.Getenv("GITEA_TOKEN")
|
|
giteaURL := os.Getenv("GITEA_URL")
|
|
giteaOwner := os.Getenv("GITEA_OWNER")
|
|
giteaRepo := os.Getenv("GITEA_REPO")
|
|
if giteaToken == "" || giteaURL == "" || giteaOwner == "" || giteaRepo == "" {
|
|
panic("GITEA_TOKEN, GITEA_URL, GITEA_OWNER, GITEA_REPO must be set in .env")
|
|
}
|
|
|
|
// 1. Get or create the release
|
|
releaseID, err := getOrCreateRelease(giteaURL, giteaOwner, giteaRepo, *releaseTag, giteaToken)
|
|
if err != nil {
|
|
panic("Failed to get or create release: " + err.Error())
|
|
}
|
|
|
|
// 2. Upload each binary as an asset
|
|
for _, bin := range builtBinaries {
|
|
fmt.Printf("Uploading %s...\n", bin)
|
|
err := uploadAsset(giteaURL, giteaOwner, giteaRepo, releaseID, bin, giteaToken)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Failed to upload %s: %v", bin, err))
|
|
}
|
|
fmt.Printf("Uploaded %s\n", bin)
|
|
}
|
|
fmt.Println("All binaries uploaded to Gitea release.")
|
|
|
|
// 3. Tag the release locally and push the tag
|
|
if err := tagAndPush(*releaseTag); err != nil {
|
|
panic("Failed to tag and push: " + err.Error())
|
|
}
|
|
fmt.Printf("Tagged and pushed %s\n", *releaseTag)
|
|
}
|
|
}
|
|
|
|
func getOrCreateRelease(url, owner, repo, tag, token string) (int, error) {
|
|
// Try to get the release by tag
|
|
api := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/tags/%s", strings.TrimRight(url, "/"), owner, repo, tag)
|
|
req, _ := http.NewRequest("GET", api, nil)
|
|
req.Header.Set("Authorization", "token "+token)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == 200 {
|
|
// Parse JSON to get ID
|
|
type releaseResp struct{ ID int }
|
|
var r releaseResp
|
|
io.ReadAll(resp.Body) // ignore body for now, parse below
|
|
dec := json.NewDecoder(resp.Body)
|
|
dec.Decode(&r)
|
|
return r.ID, nil
|
|
}
|
|
// If not found, create it
|
|
if resp.StatusCode == 404 {
|
|
api = fmt.Sprintf("%s/api/v1/repos/%s/%s/releases", strings.TrimRight(url, "/"), owner, repo)
|
|
body := strings.NewReader(fmt.Sprintf(`{"tag_name":"%s","name":"%s"}`, tag, tag))
|
|
req, _ = http.NewRequest("POST", api, body)
|
|
req.Header.Set("Authorization", "token "+token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err = http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == 201 {
|
|
type releaseResp struct{ ID int }
|
|
var r releaseResp
|
|
dec := json.NewDecoder(resp.Body)
|
|
dec.Decode(&r)
|
|
return r.ID, nil
|
|
}
|
|
return 0, fmt.Errorf("failed to create release: %s", resp.Status)
|
|
}
|
|
return 0, fmt.Errorf("failed to get release: %s", resp.Status)
|
|
}
|
|
|
|
func uploadAsset(url, owner, repo string, releaseID int, filePath, token string) error {
|
|
api := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/%d/assets?name=%s", strings.TrimRight(url, "/"), owner, repo, releaseID, filepath.Base(filePath))
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
var b bytes.Buffer
|
|
w := multipart.NewWriter(&b)
|
|
f, err := w.CreateFormFile("attachment", filepath.Base(filePath))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(f, file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.Close()
|
|
|
|
req, _ := http.NewRequest("POST", api, &b)
|
|
req.Header.Set("Authorization", "token "+token)
|
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 201 {
|
|
return fmt.Errorf("upload failed: %s", resp.Status)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func tagAndPush(tag string) error {
|
|
// Check if tag exists
|
|
cmd := exec.Command("git", "tag", "--list", tag)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(string(out)) == tag {
|
|
// Tag already exists
|
|
return nil
|
|
}
|
|
// Create tag
|
|
cmd = exec.Command("git", "tag", tag)
|
|
if err := cmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
// Push tag
|
|
cmd = exec.Command("git", "push", "origin", tag)
|
|
if err := cmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|