chore: Add signrel command for sigining arc release assets (#1426)
* chore: Add signrel command for sigining arc release assets
I used this command to sign assets for the recent releases to comply with the recommendation of 5758364c82/docs/checks.md (signed-releases)
Ref #1298
* Implement signrel subcommands for listing tags and signing assets, with docs
This commit is contained in:
parent
0e15a78541
commit
bdbcf66569
|
|
@ -0,0 +1,67 @@
|
|||
# signrel
|
||||
|
||||
`signrel` is the utility command for downloading `actions-runner-controller` release assets, sigining those, and uploading the signature files.
|
||||
|
||||
## Verifying Release Assets
|
||||
|
||||
For users, browse https://keys.openpgp.org/search?q=D8078411E3D8400B574EDB0441B69B728F095A87 and download the public key, or refer to [the instruction](https://keys.openpgp.org/about/usage#gnupg-retrieve) to import the key onto your machine.
|
||||
|
||||
Next, you'll want to verify the signature of the download asset somehow.
|
||||
|
||||
With `gpg`, you would usually do that by downloading both the asset and the signature files from our specific release page, and run `gpg --verify` like:
|
||||
|
||||
```console
|
||||
# Download the asset
|
||||
curl -LO https://github.com/actions-runner-controller/actions-runner-controller/releases/download/v0.23.0/actions-runner-controller.yaml
|
||||
|
||||
# Download the signature file
|
||||
curl -LO https://github.com/actions-runner-controller/actions-runner-controller/releases/download/v0.23.0/actions-runner-controller.yaml.asc
|
||||
|
||||
# Verify
|
||||
gpg --verify actions-runner-controller.yaml{.asc,}
|
||||
```
|
||||
|
||||
On succesful verification, the gpg command would output:
|
||||
|
||||
```
|
||||
gpg: Signature made Tue 10 May 2022 04:15:32 AM UTC
|
||||
gpg: using RSA key D8078411E3D8400B574EDB0441B69B728F095A87
|
||||
gpg: checking the trustdb
|
||||
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
|
||||
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
|
||||
gpg: next trustdb check due at 2024-05-09
|
||||
gpg: Good signature from "Yusuke Kuoka <ykuoka@gmail.com>" [ultimate]
|
||||
```
|
||||
|
||||
## Signing Release Assets
|
||||
|
||||
Assuming you are a maintainer of the project who has admin permission, run the command like the below to sign assets and upload the signature files:
|
||||
|
||||
```console
|
||||
$ cd hack/signrel
|
||||
|
||||
$ for v in v0.23.0 actions-runner-controller-0.18.0 v0.22.3 v0.22.2 actions-runner-controller-0.17.2; do TAG=$v go run . sign; done
|
||||
|
||||
Downloading actions-runner-controller.yaml to downloads/v0.23.0/actions-runner-controller.yaml
|
||||
Uploading downloads/v0.23.0/actions-runner-controller.yaml.asc
|
||||
downloads/v0.23.0/actions-runner-controller.yaml.asc has been already uploaded
|
||||
Downloading actions-runner-controller-0.18.0.tgz to downloads/actions-runner-controller-0.18.0/actions-runner-controller-0.18.0.tgz
|
||||
Uploading downloads/actions-runner-controller-0.18.0/actions-runner-controller-0.18.0.tgz.asc
|
||||
Upload completed: *snip*
|
||||
Downloading actions-runner-controller.yaml to downloads/v0.22.3/actions-runner-controller.yaml
|
||||
Uploading downloads/v0.22.3/actions-runner-controller.yaml.asc
|
||||
Upload completed: *snip*
|
||||
Downloading actions-runner-controller.yaml to downloads/v0.22.2/actions-runner-controller.yaml
|
||||
Uploading downloads/v0.22.2/actions-runner-controller.yaml.asc
|
||||
Upload completed: *snip*
|
||||
Downloading actions-runner-controller-0.17.2.tgz to downloads/actions-runner-controller-0.17.2/actions-runner-controller-0.17.2.tgz
|
||||
Uploading downloads/actions-runner-controller-0.17.2/actions-runner-controller-0.17.2.tgz.asc
|
||||
Upload completed: *snip*
|
||||
actions-runner-controller-0.17.2.tgz.asc"}
|
||||
```
|
||||
|
||||
To retrieve all the available release tags, run:
|
||||
|
||||
```
|
||||
$ go run . tags | jq -r .[].tag_name
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/actions-runner-controller/actions-runner-controller/hack/sigrel
|
||||
|
||||
go 1.17
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in New Issue