326 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			326 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"log"
 | 
						|
	"net/http"
 | 
						|
	"os"
 | 
						|
	"os/exec"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
)
 | 
						|
 | 
						|
func main() {
 | 
						|
	a := &githubReleaseAsset{}
 | 
						|
 | 
						|
	if len(os.Args) != 2 {
 | 
						|
		fmt.Fprintf(os.Stderr, "Invalid command: %v\n", os.Args)
 | 
						|
		fmt.Fprintf(os.Stderr, "USAGE: signrel [list-tags|sign-assets]\n")
 | 
						|
		os.Exit(2)
 | 
						|
	}
 | 
						|
 | 
						|
	switch cmd := os.Args[1]; cmd {
 | 
						|
	case "tags":
 | 
						|
		listTags(a)
 | 
						|
	case "sign":
 | 
						|
		tag := os.Getenv("TAG")
 | 
						|
		sign(a, tag)
 | 
						|
	default:
 | 
						|
		fmt.Fprintf(os.Stderr, "Unknown command %s\n", cmd)
 | 
						|
		os.Exit(2)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func listTags(a *githubReleaseAsset) {
 | 
						|
	_, err := a.getRecentReleases(owner, repo)
 | 
						|
	if err != nil {
 | 
						|
		fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
						|
		os.Exit(1)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func sign(a *githubReleaseAsset, tag string) {
 | 
						|
	if err := a.Download(tag, "downloads"); err != nil {
 | 
						|
		fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
						|
		os.Exit(1)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type Release struct {
 | 
						|
	ID int64 `json:"id"`
 | 
						|
}
 | 
						|
 | 
						|
type Asset struct {
 | 
						|
	Name string `json:"name"`
 | 
						|
	ID   int64  `json:"id"`
 | 
						|
	URL  string `json:"url"`
 | 
						|
}
 | 
						|
 | 
						|
type AssetsResponse struct {
 | 
						|
	Assets []Asset
 | 
						|
}
 | 
						|
 | 
						|
type githubReleaseAsset struct {
 | 
						|
}
 | 
						|
 | 
						|
const (
 | 
						|
	owner = "actions-runner-controller"
 | 
						|
	repo  = "actions-runner-controller"
 | 
						|
)
 | 
						|
 | 
						|
// GetFile downloads the give URL into the given path. The URL must
 | 
						|
// reference a single file. If possible, the Getter should check if
 | 
						|
// the remote end contains the same file and no-op this operation.
 | 
						|
func (a *githubReleaseAsset) Download(tag string, dstDir string) error {
 | 
						|
	release, err := a.getReleaseByTag(owner, repo, tag)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	assets, err := a.getAssetsByReleaseID(owner, repo, release.ID)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	d := filepath.Join(dstDir, tag)
 | 
						|
	if err := os.MkdirAll(d, 0755); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, asset := range assets {
 | 
						|
		if strings.HasSuffix(asset.Name, ".asc") {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		p := filepath.Join(d, asset.Name)
 | 
						|
		fmt.Fprintf(os.Stderr, "Downloading %s to %s\n", asset.Name, p)
 | 
						|
		if err := a.getFile(p, owner, repo, asset.ID); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		if info, _ := os.Stat(p + ".asc"); info == nil {
 | 
						|
			_, err := a.sign(p)
 | 
						|
			if err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		sig := p + ".asc"
 | 
						|
 | 
						|
		fmt.Fprintf(os.Stdout, "Uploading %s\n", sig)
 | 
						|
 | 
						|
		if err := a.upload(sig, release.ID); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (a *githubReleaseAsset) getRecentReleases(owner, repo string) (*Release, error) {
 | 
						|
	reqURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
 | 
						|
 | 
						|
	req, err := http.NewRequest("GET", reqURL, nil)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if gt := os.Getenv("GITHUB_TOKEN"); gt != "" {
 | 
						|
		req.Header = make(http.Header)
 | 
						|
		req.Header.Add("authorization", "token "+gt)
 | 
						|
	}
 | 
						|
 | 
						|
	res, err := http.DefaultClient.Do(req)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer res.Body.Close()
 | 
						|
 | 
						|
	if res.StatusCode != http.StatusOK {
 | 
						|
		return nil, fmt.Errorf("GET %s: %s", reqURL, res.Status)
 | 
						|
	}
 | 
						|
 | 
						|
	body, err := io.ReadAll(res.Body)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	fmt.Fprintf(os.Stdout, "%s\n", string(body))
 | 
						|
 | 
						|
	return nil, nil
 | 
						|
}
 | 
						|
 | 
						|
func (a *githubReleaseAsset) getReleaseByTag(owner, repo, tag string) (*Release, error) {
 | 
						|
	var release Release
 | 
						|
 | 
						|
	reqURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", owner, repo, tag)
 | 
						|
 | 
						|
	req, err := http.NewRequest("GET", reqURL, nil)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if gt := os.Getenv("GITHUB_TOKEN"); gt != "" {
 | 
						|
		req.Header = make(http.Header)
 | 
						|
		req.Header.Add("authorization", "token "+gt)
 | 
						|
	}
 | 
						|
 | 
						|
	res, err := http.DefaultClient.Do(req)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer res.Body.Close()
 | 
						|
 | 
						|
	if res.StatusCode != http.StatusOK {
 | 
						|
		return nil, fmt.Errorf("GET %s: %s", reqURL, res.Status)
 | 
						|
	}
 | 
						|
 | 
						|
	d := json.NewDecoder(res.Body)
 | 
						|
 | 
						|
	if err := d.Decode(&release); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return &release, nil
 | 
						|
}
 | 
						|
 | 
						|
func (a *githubReleaseAsset) getAssetsByReleaseID(owner, repo string, releaseID int64) ([]Asset, error) {
 | 
						|
	var assets []Asset
 | 
						|
 | 
						|
	reqURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/%d/assets", owner, repo, releaseID)
 | 
						|
 | 
						|
	req, err := http.NewRequest("GET", reqURL, nil)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if gt := os.Getenv("GITHUB_TOKEN"); gt != "" {
 | 
						|
		req.Header = make(http.Header)
 | 
						|
		req.Header.Add("authorization", "token "+gt)
 | 
						|
	}
 | 
						|
 | 
						|
	res, err := http.DefaultClient.Do(req)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer res.Body.Close()
 | 
						|
 | 
						|
	if res.StatusCode != http.StatusOK {
 | 
						|
		return nil, fmt.Errorf("GET %s: %s", reqURL, res.Status)
 | 
						|
	}
 | 
						|
 | 
						|
	d := json.NewDecoder(res.Body)
 | 
						|
 | 
						|
	if err := d.Decode(&assets); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return assets, nil
 | 
						|
}
 | 
						|
 | 
						|
func (a *githubReleaseAsset) getFile(dst string, owner, repo string, assetID int64) error {
 | 
						|
	// Create all the parent directories if needed
 | 
						|
	dir := filepath.Dir(dst)
 | 
						|
	if err := os.MkdirAll(dir, 0755); err != nil {
 | 
						|
		return fmt.Errorf("mkdir %s: %w", dir, err)
 | 
						|
	}
 | 
						|
 | 
						|
	req, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/assets/%d", owner, repo, assetID), nil)
 | 
						|
	if err != nil {
 | 
						|
		log.Fatal(err)
 | 
						|
	}
 | 
						|
	req.Header = make(http.Header)
 | 
						|
	if gt := os.Getenv("GITHUB_TOKEN"); gt != "" {
 | 
						|
		req.Header.Add("authorization", "token "+gt)
 | 
						|
	}
 | 
						|
	req.Header.Add("accept", "application/octet-stream")
 | 
						|
 | 
						|
	res, err := http.DefaultClient.Do(req)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer res.Body.Close()
 | 
						|
 | 
						|
	f, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, os.FileMode(0666))
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("open file %s: %w", dst, err)
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
 | 
						|
	if _, err := io.Copy(f, res.Body); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (a *githubReleaseAsset) sign(path string) (string, error) {
 | 
						|
	pass := os.Getenv("SIGNREL_PASSWORD")
 | 
						|
	cmd := exec.Command("gpg", "--armor", "--detach-sign", "--pinentry-mode", "loopback", "--passphrase", pass, path)
 | 
						|
	cap, err := cmd.CombinedOutput()
 | 
						|
	if err != nil {
 | 
						|
		fmt.Fprintf(os.Stderr, "gpg: %s", string(cap))
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	return path + ".asc", nil
 | 
						|
}
 | 
						|
 | 
						|
func (a *githubReleaseAsset) upload(sig string, releaseID int64) error {
 | 
						|
	assetName := filepath.Base(sig)
 | 
						|
	url := fmt.Sprintf("https://uploads.github.com/repos/%s/%s/releases/%d/assets?name=%s", owner, repo, releaseID, assetName)
 | 
						|
	f, err := os.Open(sig)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
 | 
						|
	size, err := f.Seek(0, 2)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	_, err = f.Seek(0, 0)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	req, err := http.NewRequest("POST", url, f)
 | 
						|
	if err != nil {
 | 
						|
		log.Fatal(err)
 | 
						|
	}
 | 
						|
	req.Header = make(http.Header)
 | 
						|
	if gt := os.Getenv("GITHUB_TOKEN"); gt != "" {
 | 
						|
		req.Header.Add("authorization", "token "+gt)
 | 
						|
	}
 | 
						|
	req.Header.Add("content-type", "application/octet-stream")
 | 
						|
 | 
						|
	req.ContentLength = size
 | 
						|
	req.Header.Add("accept", "application/vnd.github.v3+json")
 | 
						|
 | 
						|
	// blob, _ := httputil.DumpRequestOut(req, true)
 | 
						|
	// fmt.Printf("%s\n", blob)
 | 
						|
 | 
						|
	res, err := http.DefaultClient.Do(req)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer res.Body.Close()
 | 
						|
 | 
						|
	body, err := io.ReadAll(res.Body)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if res.StatusCode == 422 {
 | 
						|
		fmt.Fprintf(os.Stdout, "%s has been already uploaded\n", sig)
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	if res.StatusCode >= 300 {
 | 
						|
		return fmt.Errorf("unexpected http status %d: %s", res.StatusCode, body)
 | 
						|
	}
 | 
						|
 | 
						|
	fmt.Fprintf(os.Stdout, "Upload completed: %s\n", body)
 | 
						|
 | 
						|
	return err
 | 
						|
}
 |