Expose tagged-source delivery through facade and CLI
This commit is contained in:
@@ -3,8 +3,13 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -234,6 +239,348 @@ func newCLIRepository(t *testing.T) string {
|
||||
return repository
|
||||
}
|
||||
|
||||
func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
state := &deliveryServerState{}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
settings := cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}
|
||||
writeDeliveryProfile(t, repository, settings)
|
||||
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
||||
cliGitRun(t, repository, "tag", "v1.2.3") // Historical lightweight tags are valid delivery inputs.
|
||||
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
||||
if status != 0 {
|
||||
t.Fatalf("delivery attempt %d status=%d stdout=%q stderr=%q", attempt+1, status, stdout.String(), stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "state complete") || !strings.Contains(stdout.String(), "release: "+server.URL+"/releases/10") {
|
||||
t.Fatalf("delivery output = %q", stdout.String())
|
||||
}
|
||||
}
|
||||
if state.releaseCreates != 1 || state.assetUploads != 0 || state.assetListReads != 0 {
|
||||
t.Fatalf("page-only calls = creates:%d asset uploads:%d asset reads:%d", state.releaseCreates, state.assetUploads, state.assetListReads)
|
||||
}
|
||||
if state.noteBody != "# Example Product v1.2.3\n\nRelease note.\n" {
|
||||
t.Fatalf("hosted note body = %q", state.noteBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverBuildsTaggedArtifactsAndReusesAssets(t *testing.T) {
|
||||
repository := newDeliveryBuildRepository(t)
|
||||
state := &deliveryServerState{}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
settings := cli.Settings{
|
||||
Product: "Example Product",
|
||||
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}},
|
||||
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
||||
}
|
||||
writeDeliveryProfile(t, repository, settings)
|
||||
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
||||
cliGitRun(t, repository, "tag", "v1.2.3")
|
||||
cliGitRun(t, repository, "checkout", "--detach", "v1.2.3")
|
||||
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
||||
if status != 0 {
|
||||
t.Fatalf("delivery attempt %d status=%d stdout=%q stderr=%q", attempt+1, status, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
if state.releaseCreates != 1 || state.assetUploads != 2 {
|
||||
t.Fatalf("delivery calls = creates:%d asset uploads:%d", state.releaseCreates, state.assetUploads)
|
||||
}
|
||||
if _, ok := state.assets["helper-v1.2.3-linux-amd64"]; !ok {
|
||||
t.Fatalf("binary was not uploaded: %#v", state.assets)
|
||||
}
|
||||
if _, ok := state.assets["SHA256SUMS"]; !ok {
|
||||
t.Fatalf("checksum manifest was not uploaded: %#v", state.assets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverRejectsWrongHostedTagBeforeReleaseWrite(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
state := &deliveryServerState{remoteCommit: strings.Repeat("0", 40)}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
|
||||
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
||||
cliGitRun(t, repository, "tag", "v1.2.3")
|
||||
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
||||
if status != 1 || state.releaseCreates != 0 || state.assetUploads != 0 {
|
||||
t.Fatalf("wrong hosted tag status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.releaseCreates, state.assetUploads, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverRequiresCredentialBeforeRemoteRequests(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
state := &deliveryServerState{}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example", TokenEnv: "RELEASEKIT_TEST_DELIVERY_TOKEN"}})
|
||||
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
||||
cliGitRun(t, repository, "tag", "v1.2.3")
|
||||
t.Setenv("RELEASEKIT_TEST_DELIVERY_TOKEN", "")
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
||||
if status != 2 || state.apiRequests != 0 || !strings.Contains(stderr.String(), "RELEASEKIT_TEST_DELIVERY_TOKEN") {
|
||||
t.Fatalf("missing credential status=%d requests=%d stdout=%q stderr=%q", status, state.apiRequests, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverRejectsCheckoutThatDiffersFromLocalTag(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
state := &deliveryServerState{}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
|
||||
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
||||
cliGitRun(t, repository, "tag", "v1.2.3")
|
||||
if err := os.WriteFile(filepath.Join(repository, "later.txt"), []byte("later commit"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cliGitRun(t, repository, "add", "later.txt")
|
||||
cliGitRun(t, repository, "commit", "-m", "advance checkout")
|
||||
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
||||
if status != 1 || state.apiRequests != 0 || state.releaseCreates != 0 {
|
||||
t.Fatalf("mismatched checkout status=%d requests=%d creates=%d stdout=%q stderr=%q", status, state.apiRequests, state.releaseCreates, stdout.String(), stderr.String())
|
||||
}
|
||||
if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3"); got != state.commit {
|
||||
t.Fatalf("delivery moved tag from %s to %s", state.commit, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testing.T) {
|
||||
for _, mode := range []string{"valid receipt", "tampered", "stale settings"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
state := &deliveryServerState{}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
current := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
|
||||
settings := cli.Settings{Product: "Example Product", Build: ¤t, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}
|
||||
writeDeliveryProfile(t, repository, settings)
|
||||
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
||||
cliGitRun(t, repository, "tag", "v1.2.3")
|
||||
receiptSettings := current
|
||||
if mode == "stale settings" {
|
||||
receiptSettings.BinaryName = "previous"
|
||||
}
|
||||
assetsDir, artifactPath := writeDeliveryReceipt(t, repository, receiptSettings)
|
||||
if mode == "tampered" {
|
||||
if err := os.WriteFile(artifactPath, []byte("tampered artifact"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr)
|
||||
if mode == "valid receipt" {
|
||||
if status != 0 || state.releaseCreates != 1 || state.assetUploads != 2 {
|
||||
t.Fatalf("valid receipt status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.releaseCreates, state.assetUploads, stdout.String(), stderr.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
if status != 1 || state.apiRequests != 0 || state.releaseCreates != 0 || state.assetUploads != 0 {
|
||||
t.Fatalf("%s receipt status=%d requests=%d stdout=%q stderr=%q", mode, status, state.apiRequests, stdout.String(), stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeDeliveryReceipt(t *testing.T, repository string, settings cli.BuildSettings) (string, string) {
|
||||
t.Helper()
|
||||
assetsDir := t.TempDir()
|
||||
filename := settings.BinaryName + "-v1.2.3-linux-amd64"
|
||||
artifactPath := filepath.Join(assetsDir, filename)
|
||||
content := []byte("fixture artifact")
|
||||
if err := os.WriteFile(artifactPath, content, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resolvedRepository, err := filepath.Abs(repository)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256(content)
|
||||
target := cli.ReceiptTarget{OS: "linux", Arch: "amd64"}
|
||||
receipt := cli.BuildReceipt{
|
||||
RepositoryDir: resolvedRepository, CandidateCommit: cliGitOutput(t, repository, "rev-parse", "HEAD"),
|
||||
Version: "v1.2.3", MainPackage: settings.MainPackage, BinaryName: settings.BinaryName,
|
||||
OutputDir: assetsDir, Targets: []cli.ReceiptTarget{target}, Compiler: "go version fixture",
|
||||
Artifacts: []cli.ReceiptArtifact{{Path: artifactPath, Filename: filename, Target: target, SHA256: hex.EncodeToString(digest[:])}},
|
||||
}
|
||||
if _, err := cli.WriteBuildReceipt(assetsDir, receipt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return assetsDir, artifactPath
|
||||
}
|
||||
|
||||
type deliveryServerState struct {
|
||||
commit string
|
||||
remoteCommit string
|
||||
apiRequests int
|
||||
pageExists bool
|
||||
releaseCreates int
|
||||
assetUploads int
|
||||
assetListReads int
|
||||
noteBody string
|
||||
assets map[string][]byte
|
||||
}
|
||||
|
||||
func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server {
|
||||
t.Helper()
|
||||
if state.assets == nil {
|
||||
state.assets = make(map[string][]byte)
|
||||
}
|
||||
serverURL := ""
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
state.apiRequests++
|
||||
if r.Header.Get("Authorization") != "token fixture-token" {
|
||||
t.Errorf("authorization = %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/repos/owner/example/tags/v1.2.3":
|
||||
if state.commit == "" {
|
||||
t.Error("tag source commit was not initialized")
|
||||
}
|
||||
commit := state.remoteCommit
|
||||
if commit == "" {
|
||||
commit = state.commit
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, `{"name":"v1.2.3","commit":{"sha":%q}}`, commit)
|
||||
case "/api/v1/repos/owner/example/releases/tags/v1.2.3":
|
||||
if !state.pageExists {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
writeDeliveryRelease(w, serverURL, state)
|
||||
case "/api/v1/repos/owner/example/releases":
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("release request method = %s", r.Method)
|
||||
http.Error(w, "method", 405)
|
||||
return
|
||||
}
|
||||
var payload struct {
|
||||
Tag string `json:"tag_name"`
|
||||
Target string `json:"target_commitish"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Errorf("decode create payload: %v", err)
|
||||
}
|
||||
if payload.Tag != "v1.2.3" || payload.Target != state.commit {
|
||||
t.Errorf("release source payload = %#v", payload)
|
||||
}
|
||||
state.pageExists, state.releaseCreates, state.noteBody = true, state.releaseCreates+1, payload.Body
|
||||
writeDeliveryRelease(w, serverURL, state)
|
||||
case "/api/v1/repos/owner/example/releases/10/assets":
|
||||
if r.Method == http.MethodGet {
|
||||
state.assetListReads++
|
||||
list := make([]map[string]any, 0, len(state.assets))
|
||||
for name, content := range state.assets {
|
||||
list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": serverURL + "/files/" + name})
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(list)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("asset request method = %s", r.Method)
|
||||
http.Error(w, "method", 405)
|
||||
return
|
||||
}
|
||||
name := r.URL.Query().Get("name")
|
||||
file, _, err := r.FormFile("attachment")
|
||||
if err != nil {
|
||||
t.Errorf("read asset upload %q: %v", name, err)
|
||||
http.Error(w, "multipart", 400)
|
||||
return
|
||||
}
|
||||
content, err := io.ReadAll(file)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
t.Errorf("read asset bytes: %v", err)
|
||||
}
|
||||
if _, exists := state.assets[name]; exists {
|
||||
t.Errorf("asset %q uploaded more than once", name)
|
||||
}
|
||||
state.assets[name] = content
|
||||
state.assetUploads++
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": state.assetUploads, "name": name, "size": len(content)})
|
||||
default:
|
||||
if strings.HasPrefix(r.URL.Path, "/files/") {
|
||||
name := strings.TrimPrefix(r.URL.Path, "/files/")
|
||||
content, ok := state.assets[name]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(content)
|
||||
return
|
||||
}
|
||||
t.Errorf("unexpected delivery request %s %s", r.Method, r.URL.String())
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
server := httptest.NewServer(handler)
|
||||
serverURL = server.URL
|
||||
return server
|
||||
}
|
||||
|
||||
func writeDeliveryRelease(w http.ResponseWriter, serverURL string, state *deliveryServerState) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL + "/releases/10", "tag_name": "v1.2.3", "target_commitish": state.commit, "name": "Example Product v1.2.3", "body": state.noteBody, "draft": false, "prerelease": false})
|
||||
}
|
||||
|
||||
func writeDeliveryProfile(t *testing.T, repository string, settings cli.Settings) {
|
||||
t.Helper()
|
||||
contents, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repository, "releasekit.json"), contents, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cliGitRun(t, repository, "add", "releasekit.json")
|
||||
cliGitRun(t, repository, "commit", "-m", "configure delivery")
|
||||
}
|
||||
|
||||
func newDeliveryBuildRepository(t *testing.T) string {
|
||||
t.Helper()
|
||||
repository := filepath.Join(t.TempDir(), "tagged application")
|
||||
if err := os.Mkdir(repository, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cliGitRun(t, repository, "init", "--initial-branch=main")
|
||||
cliGitRun(t, repository, "config", "user.name", "ReleaseKit Test")
|
||||
cliGitRun(t, repository, "config", "user.email", "releasekit@example.test")
|
||||
files := map[string]string{
|
||||
"go.mod": "module example.com/tagged-app\n\ngo 1.25.0\n",
|
||||
"main.go": "package main\nimport \"fmt\"\nfunc main() { fmt.Println(\"helper v1.2.3\") }\n",
|
||||
"docs/releases/v1.2.3.md": "# Example Product v1.2.3\n\nRelease note.\n",
|
||||
}
|
||||
for name, contents := range files {
|
||||
path := filepath.Join(repository, filepath.FromSlash(name))
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
cliGitRun(t, repository, "add", "--all")
|
||||
cliGitRun(t, repository, "commit", "-m", "tagged candidate")
|
||||
return repository
|
||||
}
|
||||
|
||||
func newCLIBuildRepository(t *testing.T, mainSource string) string {
|
||||
t.Helper()
|
||||
repository := newCLIRepository(t)
|
||||
|
||||
Reference in New Issue
Block a user