Read delivery tokens from files and check selected paths before release
This commit is contained in:
@@ -153,8 +153,9 @@ type Settings struct {
|
||||
type ValidationSettings struct {
|
||||
// SharedFiles selects committed files relative to the repository root.
|
||||
// Nil uses optional defaults; a non-nil empty slice selects no shared files.
|
||||
SharedFiles []string `json:"shared_files"`
|
||||
Go GoValidationSettings `json:"go,omitempty"`
|
||||
SharedFiles []string `json:"shared_files"`
|
||||
UntrackedFiles []string `json:"untracked_files,omitempty"`
|
||||
Go GoValidationSettings `json:"go,omitempty"`
|
||||
// GoFormat checks the formatting of committed regular Go source files.
|
||||
// Omitted or false disables the check.
|
||||
GoFormat bool `json:"go_format,omitempty"`
|
||||
@@ -201,6 +202,7 @@ type DeliverySettings struct {
|
||||
Owner string `json:"owner"`
|
||||
Repository string `json:"repository"`
|
||||
TokenEnv string `json:"token_env,omitempty"`
|
||||
TokenFile string `json:"token_file,omitempty"`
|
||||
}
|
||||
|
||||
// BuildSettings selects the Go application package, artifact name, and targets.
|
||||
|
||||
@@ -181,8 +181,9 @@ func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec
|
||||
return releasekit.ValidationSpec{}
|
||||
}
|
||||
result := releasekit.ValidationSpec{
|
||||
SharedFiles: copyStringsPreservingEmpty(settings.SharedFiles),
|
||||
GoFormat: settings.GoFormat,
|
||||
SharedFiles: copyStringsPreservingEmpty(settings.SharedFiles),
|
||||
UntrackedFiles: copyStringsPreservingEmpty(settings.UntrackedFiles),
|
||||
GoFormat: settings.GoFormat,
|
||||
ModuleDistribution: releasekit.ModuleDistributionValidationSpec{
|
||||
WorkspaceFiles: settings.ModuleDistribution.WorkspaceFiles,
|
||||
LocalReplacements: settings.ModuleDistribution.LocalReplacements,
|
||||
@@ -290,7 +291,7 @@ func runCIDelivery(ctx context.Context, invocation cli.Invocation, stdout, stder
|
||||
fmt.Fprintf(stderr, "profile module selection: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
destination, status := deliveryDestination(settings, stderr)
|
||||
destination, status := deliveryDestination(invocation.Repository, settings, stderr)
|
||||
if status != 0 {
|
||||
return status
|
||||
}
|
||||
@@ -309,14 +310,14 @@ func runCIDelivery(ctx context.Context, invocation cli.Invocation, stdout, stder
|
||||
// deliverWithSettings adapts prepared CLI state to the public delivery facade.
|
||||
// CI entry points can reuse it after preparing the checkout and loading its profile.
|
||||
func deliverWithSettings(ctx context.Context, invocation cli.Invocation, settings cli.Settings, stdout, stderr io.Writer) int {
|
||||
destination, status := deliveryDestination(settings, stderr)
|
||||
destination, status := deliveryDestination(invocation.Repository, settings, stderr)
|
||||
if status != 0 {
|
||||
return status
|
||||
}
|
||||
return deliverWithDestination(ctx, invocation, settings, destination, stdout, stderr)
|
||||
}
|
||||
|
||||
func deliveryDestination(settings cli.Settings, stderr io.Writer) (releasekit.GiteaDestination, int) {
|
||||
func deliveryDestination(repositoryDir string, settings cli.Settings, stderr io.Writer) (releasekit.GiteaDestination, int) {
|
||||
delivery := settings.Delivery
|
||||
if delivery == nil || strings.TrimSpace(delivery.BaseURL) == "" || strings.TrimSpace(delivery.Owner) == "" || strings.TrimSpace(delivery.Repository) == "" {
|
||||
fmt.Fprintln(stderr, "deliver requires base_url, owner, and repository in the profile delivery settings")
|
||||
@@ -327,7 +328,23 @@ func deliveryDestination(settings cli.Settings, stderr io.Writer) (releasekit.Gi
|
||||
tokenEnv = "RELEASEKIT_GITEA_TOKEN"
|
||||
}
|
||||
token, ok := os.LookupEnv(tokenEnv)
|
||||
if !ok || strings.TrimSpace(token) == "" {
|
||||
if (!ok || strings.TrimSpace(token) == "") && delivery.TokenFile != "" {
|
||||
name := delivery.TokenFile
|
||||
if !filepath.IsAbs(name) {
|
||||
name = filepath.Join(repositoryDir, name)
|
||||
}
|
||||
contents, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "read delivery token file: %v\n", err)
|
||||
return releasekit.GiteaDestination{}, 2
|
||||
}
|
||||
token = strings.TrimSpace(string(contents))
|
||||
if token == "" {
|
||||
fmt.Fprintln(stderr, "delivery token file is empty or blank")
|
||||
return releasekit.GiteaDestination{}, 2
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(token) == "" {
|
||||
fmt.Fprintf(stderr, "deliver requires a non-empty token in environment variable %s\n", tokenEnv)
|
||||
return releasekit.GiteaDestination{}, 2
|
||||
}
|
||||
|
||||
111
cmd/releasekit/token_file_test.go
Normal file
111
cmd/releasekit/token_file_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
|
||||
)
|
||||
|
||||
func TestDeliveryTokenFileSelection(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name, env, contents, file, want string
|
||||
status int
|
||||
}{
|
||||
{name: "relative file", contents: "fixture-token\n", file: "token", want: "fixture-token"},
|
||||
{name: "absolute file", contents: "fixture-token\n", file: "absolute", want: "fixture-token"},
|
||||
{name: "environment overrides missing file", env: "env-token", file: "missing", want: "env-token"},
|
||||
{name: "blank environment falls back", env: " \t", contents: "fixture-token\n", file: "token", want: "fixture-token"},
|
||||
{name: "missing file", file: "missing", status: 2},
|
||||
{name: "blank file", contents: " \n\t", file: "token", status: 2},
|
||||
{name: "empty file", file: "token", status: 2},
|
||||
{name: "unreadable as file", file: ".", status: 2},
|
||||
{name: "no credential", status: 2},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
repository := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(repository, "token"), []byte(test.contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := test.file
|
||||
if name == "absolute" {
|
||||
name = filepath.Join(repository, "token")
|
||||
}
|
||||
t.Setenv("RELEASEKIT_TEST_TOKEN", test.env)
|
||||
var stderr bytes.Buffer
|
||||
destination, status := deliveryDestination(repository, cli.Settings{Delivery: &cli.DeliverySettings{
|
||||
BaseURL: "https://gitea.example", Owner: "owner", Repository: "example",
|
||||
TokenEnv: "RELEASEKIT_TEST_TOKEN", TokenFile: name,
|
||||
}}, &stderr)
|
||||
if status != test.status || destination.Token != test.want {
|
||||
t.Fatalf("status=%d token matches=%t, want status=%d; diagnostics=%s", status, destination.Token == test.want, test.status, &stderr)
|
||||
}
|
||||
for _, secret := range []string{"fixture-token", "env-token"} {
|
||||
if strings.Contains(stderr.String(), secret) {
|
||||
t.Fatal("diagnostics exposed a token")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckReadsUntrackedFilesSelectionFromProfile(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
writeDeliveryProfile(t, repository, cli.Settings{
|
||||
Product: "Example Product",
|
||||
Validation: &cli.ValidationSettings{UntrackedFiles: []string{"token-file"}},
|
||||
Delivery: &cli.DeliverySettings{BaseURL: "https://gitea.example", Owner: "owner", Repository: "example", TokenFile: "missing-token-file"},
|
||||
})
|
||||
writeRepositoryFile(t, repository, "token-file", "fixture-secret\n")
|
||||
cliGitRun(t, repository, "add", "token-file")
|
||||
cliGitRun(t, repository, "commit", "-m", "capture a selected token path")
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"check", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
||||
if status != 1 || !strings.Contains(stderr.String(), "untracked-files") || !strings.Contains(stderr.String(), "token-file") {
|
||||
t.Fatalf("check status=%d output=%s diagnostics=%s", status, &stdout, &stderr)
|
||||
}
|
||||
if strings.Contains(stdout.String()+stderr.String(), "fixture-secret") {
|
||||
t.Fatal("check exposed the token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverAndCIDeliverReadTokenFileFromRepositoryRoot(t *testing.T) {
|
||||
for _, command := range []string{"deliver", "ci-deliver"} {
|
||||
t.Run(command, func(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", TokenFile: ".releasekit/token"},
|
||||
})
|
||||
writeRepositoryFile(t, repository, ".gitignore", ".releasekit/\n")
|
||||
cliGitRun(t, repository, "add", ".gitignore")
|
||||
cliGitRun(t, repository, "commit", "-m", "ignore local delivery credentials")
|
||||
commit := pushTaggedCLIRepository(t, repository)
|
||||
state.update(func(data *deliveryServerData) { data.commit = commit })
|
||||
writeRepositoryFile(t, repository, ".releasekit/token", "fixture-token\n")
|
||||
t.Setenv("RELEASEKIT_GITEA_TOKEN", "")
|
||||
var stdout, stderr bytes.Buffer
|
||||
args := []string{command, "--repo", filepath.Join(repository, ".releasekit")}
|
||||
if command == "ci-deliver" {
|
||||
args = append(args, "--commit", commit)
|
||||
}
|
||||
args = append(args, "v1.2.3")
|
||||
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
||||
t.Fatalf("delivery status=%d output=%s diagnostics=%s", status, &stdout, &stderr)
|
||||
}
|
||||
if state.snapshot().releaseCreates != 1 {
|
||||
t.Fatal("token-file delivery did not create the release page")
|
||||
}
|
||||
if strings.Contains(stdout.String()+stderr.String(), "fixture-token") {
|
||||
t.Fatal("delivery exposed the token")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user