Deliver module releases to full-tag pages
This commit is contained in:
@@ -32,6 +32,9 @@ type ReceiptArtifact struct {
|
||||
// BuildReceipt is private local coordination data for generated binaries.
|
||||
type BuildReceipt struct {
|
||||
RepositoryDir string `json:"repository_dir"`
|
||||
ModuleDir string `json:"module_dir"`
|
||||
ModulePath string `json:"module_path"`
|
||||
Tag string `json:"tag"`
|
||||
CandidateCommit string `json:"candidate_commit"`
|
||||
SourceVerified bool `json:"source_verified"`
|
||||
Version string `json:"version"`
|
||||
@@ -144,8 +147,11 @@ func validateBuildReceipt(receipt BuildReceipt, verifyFiles bool) error {
|
||||
if !versioncontract.ValidStableVersion(receipt.Version) {
|
||||
return fmt.Errorf("build receipt has invalid version %q", receipt.Version)
|
||||
}
|
||||
if !filepath.IsAbs(receipt.RepositoryDir) || !filepath.IsAbs(receipt.OutputDir) {
|
||||
return errors.New("build receipt repository and output directories must be absolute")
|
||||
if !filepath.IsAbs(receipt.RepositoryDir) || !filepath.IsAbs(receipt.ModuleDir) || !filepath.IsAbs(receipt.OutputDir) {
|
||||
return errors.New("build receipt repository, module, and output directories must be absolute")
|
||||
}
|
||||
if strings.TrimSpace(receipt.ModulePath) == "" || strings.TrimSpace(receipt.Tag) == "" {
|
||||
return errors.New("build receipt is missing module path or full tag")
|
||||
}
|
||||
if !validCommitID(receipt.CandidateCommit) {
|
||||
return errors.New("build receipt has invalid candidate commit")
|
||||
|
||||
@@ -21,6 +21,9 @@ func TestBuildReceiptRoundTripBindsFilesAndBuildInputs(t *testing.T) {
|
||||
digest := sha256.Sum256(contents)
|
||||
receipt := BuildReceipt{
|
||||
RepositoryDir: repositoryDir,
|
||||
ModuleDir: repositoryDir,
|
||||
ModulePath: "example.com/receipt",
|
||||
Tag: "v1.2.3",
|
||||
CandidateCommit: strings.Repeat("a", 40),
|
||||
Version: "v1.2.3",
|
||||
MainPackage: "./cmd/example",
|
||||
|
||||
@@ -359,7 +359,8 @@ func writeDeliveryRecovery(output io.Writer, result releasekit.DeliveryResult) {
|
||||
|
||||
func buildResultFromReceipt(receipt cli.BuildReceipt) *releasekit.BuildResult {
|
||||
result := &releasekit.BuildResult{
|
||||
RepositoryDir: receipt.RepositoryDir, CandidateCommit: receipt.CandidateCommit, SourceVerified: receipt.SourceVerified,
|
||||
RepositoryDir: receipt.RepositoryDir, ModuleDir: receipt.ModuleDir, ModulePath: receipt.ModulePath, Tag: receipt.Tag,
|
||||
CandidateCommit: receipt.CandidateCommit, SourceVerified: receipt.SourceVerified,
|
||||
Version: receipt.Version, MainPackage: receipt.MainPackage, BinaryName: receipt.BinaryName,
|
||||
OutputDir: receipt.OutputDir, Compiler: receipt.Compiler,
|
||||
}
|
||||
@@ -394,6 +395,9 @@ func configuredBuildSpec(settings *cli.BuildSettings) *releasekit.BuildSpec {
|
||||
func receiptFromBuild(result releasekit.BuildResult) cli.BuildReceipt {
|
||||
receipt := cli.BuildReceipt{
|
||||
RepositoryDir: result.RepositoryDir,
|
||||
ModuleDir: result.ModuleDir,
|
||||
ModulePath: result.ModulePath,
|
||||
Tag: result.Tag,
|
||||
CandidateCommit: result.CandidateCommit,
|
||||
SourceVerified: result.SourceVerified,
|
||||
Version: result.Version,
|
||||
|
||||
@@ -937,9 +937,10 @@ func writeDeliveryReceipt(t *testing.T, repository string, settings cli.BuildSet
|
||||
digest := sha256.Sum256(content)
|
||||
target := cli.ReceiptTarget{OS: "linux", Arch: "amd64"}
|
||||
receipt := cli.BuildReceipt{
|
||||
RepositoryDir: resolvedRepository, CandidateCommit: cliGitOutput(t, repository, "rev-parse", "HEAD"),
|
||||
SourceVerified: true,
|
||||
Version: "v1.2.3", MainPackage: settings.MainPackage, BinaryName: settings.BinaryName,
|
||||
RepositoryDir: resolvedRepository, ModuleDir: resolvedRepository, ModulePath: "example.com/cli-candidate", Tag: "v1.2.3",
|
||||
CandidateCommit: cliGitOutput(t, repository, "rev-parse", "HEAD"),
|
||||
SourceVerified: true,
|
||||
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[:])}},
|
||||
}
|
||||
|
||||
@@ -48,9 +48,11 @@ type DeliveryRequest struct {
|
||||
RepositoryDir string
|
||||
// ModuleDir selects a repository-relative module directory; empty selects
|
||||
// the repository root.
|
||||
ModuleDir string
|
||||
Product string
|
||||
Version string
|
||||
ModuleDir string
|
||||
Product string
|
||||
Version string
|
||||
// ExpectedTag optionally binds delivery to the full tag that triggered a CI event.
|
||||
ExpectedTag string
|
||||
Destination GiteaDestination
|
||||
// Build selects binaries to build in temporary storage outside the checkout.
|
||||
Build *BuildSpec
|
||||
@@ -105,6 +107,7 @@ func Deliver(ctx context.Context, request DeliveryRequest) (DeliveryResult, erro
|
||||
ModuleDir: request.ModuleDir,
|
||||
Product: request.Product,
|
||||
Version: request.Version,
|
||||
ExpectedTag: request.ExpectedTag,
|
||||
Destination: destination,
|
||||
Build: internalBuildSpec(request.Build),
|
||||
Prebuilt: prebuilt,
|
||||
|
||||
141
delivery_test.go
141
delivery_test.go
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -190,6 +191,14 @@ func TestDeliverPageOnlySkipsBuildAndHostedAssets(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
repository := newTaggedDeliveryConsumer(t)
|
||||
prebuilt := test.prebuilt
|
||||
if prebuilt != nil {
|
||||
built, err := releasekit.Build(context.Background(), releasekit.BuildRequest{RepositoryDir: repository, Version: "v1.2.3"})
|
||||
if err != nil {
|
||||
t.Fatalf("Build() for page-only prebuilt identity = %#v, %v", built, err)
|
||||
}
|
||||
prebuilt = &built
|
||||
}
|
||||
temporaryRoot := t.TempDir()
|
||||
t.Setenv("TMPDIR", temporaryRoot)
|
||||
server, remote := newFacadeDeliveryServer(t, repository)
|
||||
@@ -202,7 +211,7 @@ func TestDeliverPageOnlySkipsBuildAndHostedAssets(t *testing.T) {
|
||||
Version: "v1.2.3",
|
||||
Destination: facadeDeliveryDestination(server.URL),
|
||||
Build: test.build,
|
||||
Prebuilt: test.prebuilt,
|
||||
Prebuilt: prebuilt,
|
||||
Output: &progress,
|
||||
})
|
||||
if err != nil || result.State != releasekit.DeliveryComplete {
|
||||
@@ -221,6 +230,106 @@ func TestDeliverPageOnlySkipsBuildAndHostedAssets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverUsesCommittedModuleIdentityForSeparatePages(t *testing.T) {
|
||||
repository := newTwoModuleDeliveryConsumer(t)
|
||||
server, remote := newFacadeDeliveryServer(t, repository)
|
||||
defer server.Close()
|
||||
|
||||
wrongTag, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||
RepositoryDir: repository,
|
||||
Product: "Fixture",
|
||||
Version: "v1.2.3",
|
||||
ExpectedTag: "integrations/wrong/v1.2.3",
|
||||
Destination: facadeDeliveryDestination(server.URL),
|
||||
})
|
||||
if err == nil || wrongTag.Tag != "v1.2.3" || remote.requests != 0 {
|
||||
t.Fatalf("delivery with inconsistent expected tag = %#v, %v; hosted requests %d", wrongTag, err, remote.requests)
|
||||
}
|
||||
|
||||
rootResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||
RepositoryDir: repository,
|
||||
Product: "Fixture",
|
||||
Version: "v1.2.3",
|
||||
ExpectedTag: "v1.2.3",
|
||||
Destination: facadeDeliveryDestination(server.URL),
|
||||
})
|
||||
if err != nil || rootResult.State != releasekit.DeliveryComplete || rootResult.Tag != "v1.2.3" {
|
||||
t.Fatalf("root delivery = %#v, %v", rootResult, err)
|
||||
}
|
||||
|
||||
nestedTag := "integrations/ckconfig/v1.2.3"
|
||||
remote.tag = nestedTag
|
||||
remote.noteBody = "# Fixture v1.2.3\n\nNested delivery fixture.\n"
|
||||
nestedResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||
RepositoryDir: repository,
|
||||
ModuleDir: "integrations/ckconfig",
|
||||
Product: "Fixture",
|
||||
Version: "v1.2.3",
|
||||
ExpectedTag: nestedTag,
|
||||
Destination: facadeDeliveryDestination(server.URL),
|
||||
})
|
||||
if err != nil || nestedResult.State != releasekit.DeliveryComplete || nestedResult.Tag != nestedTag || nestedResult.ModulePath != "example.com/ckconfig" {
|
||||
t.Fatalf("nested delivery = %#v, %v", nestedResult, err)
|
||||
}
|
||||
if len(remote.pages) != 2 || !remote.pages["v1.2.3"] || !remote.pages[nestedTag] {
|
||||
t.Fatalf("created hosted pages = %#v", remote.pages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverRejectsPrebuiltArtifactsForAnotherModuleBeforeHostedRequests(t *testing.T) {
|
||||
repository := newTwoModuleDeliveryConsumer(t)
|
||||
prebuilt, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
Spec: releasekit.BuildSpec{
|
||||
MainPackage: "./cmd/app",
|
||||
BinaryName: "fixture",
|
||||
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||
},
|
||||
OutputDir: filepath.Join(t.TempDir(), "root-artifacts"),
|
||||
})
|
||||
if err != nil || !prebuilt.SourceVerified {
|
||||
t.Fatalf("root Build() = %#v, %v", prebuilt, err)
|
||||
}
|
||||
server, remote := newFacadeDeliveryServer(t, repository)
|
||||
defer server.Close()
|
||||
remote.tag = "integrations/ckconfig/v1.2.3"
|
||||
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||
RepositoryDir: repository,
|
||||
ModuleDir: "integrations/ckconfig",
|
||||
Product: "Fixture",
|
||||
Version: "v1.2.3",
|
||||
ExpectedTag: remote.tag,
|
||||
Destination: facadeDeliveryDestination(server.URL),
|
||||
Prebuilt: &prebuilt,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "selected tagged module") || result.Tag != remote.tag {
|
||||
t.Fatalf("nested delivery with root artifacts = %#v, %v", result, err)
|
||||
}
|
||||
if remote.requests != 0 {
|
||||
t.Fatalf("hosted requests before prebuilt identity rejection = %d", remote.requests)
|
||||
}
|
||||
}
|
||||
|
||||
func newTwoModuleDeliveryConsumer(t *testing.T) string {
|
||||
t.Helper()
|
||||
repository := newTaggedDeliveryConsumer(t)
|
||||
writeNestedBuildConsumer(t, repository, "integrations/ckconfig", "example.com/ckconfig", `package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
||||
)
|
||||
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("fixture", buildinfo.Version()) } }
|
||||
`)
|
||||
writeBuildFixture(t, repository, "integrations/ckconfig/docs/releases/v1.2.3.md", "# Fixture v1.2.3\n\nNested delivery fixture.\n")
|
||||
buildGit(t, repository, "add", "--all")
|
||||
buildGit(t, repository, "commit", "-m", "add nested release module")
|
||||
buildGit(t, repository, "tag", "-f", "v1.2.3")
|
||||
buildGit(t, repository, "tag", "integrations/ckconfig/v1.2.3")
|
||||
return repository
|
||||
}
|
||||
|
||||
func TestDeliverRejectsDirtyTaggedSourceBeforeBuildOrHostedWrites(t *testing.T) {
|
||||
repository := newTaggedDeliveryConsumer(t)
|
||||
writeBuildFixture(t, repository, "local-change.txt", "dirty source")
|
||||
@@ -255,6 +364,7 @@ func TestDeliverRejectsDirtyTaggedSourceBeforeBuildOrHostedWrites(t *testing.T)
|
||||
|
||||
func TestDeliverRejectsCheckoutAwayFromTagBeforeBuildOrHostedWrites(t *testing.T) {
|
||||
repository := newTaggedDeliveryConsumer(t)
|
||||
tagCommit := buildGit(t, repository, "rev-parse", "HEAD")
|
||||
writeBuildFixture(t, repository, "later-change.txt", "new commit")
|
||||
buildGit(t, repository, "add", "--all")
|
||||
buildGit(t, repository, "commit", "-m", "advance checkout after release tag")
|
||||
@@ -276,7 +386,7 @@ func TestDeliverRejectsCheckoutAwayFromTagBeforeBuildOrHostedWrites(t *testing.T
|
||||
},
|
||||
Output: &progress,
|
||||
})
|
||||
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit == remote.commit {
|
||||
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit == tagCommit {
|
||||
t.Fatalf("Deliver() = %#v, %v; want tag/HEAD mismatch", result, err)
|
||||
}
|
||||
if progress.Len() != 0 {
|
||||
@@ -474,7 +584,10 @@ func (writer deliveryCancelWriter) Write(content []byte) (int, error) {
|
||||
type facadeDeliveryServer struct {
|
||||
t *testing.T
|
||||
commit string
|
||||
page bool
|
||||
tag string
|
||||
version string
|
||||
noteBody string
|
||||
pages map[string]bool
|
||||
requests int
|
||||
assets map[string][]byte
|
||||
url string
|
||||
@@ -482,7 +595,7 @@ type facadeDeliveryServer struct {
|
||||
|
||||
func newFacadeDeliveryServer(t *testing.T, repository string) (*httptest.Server, *facadeDeliveryServer) {
|
||||
t.Helper()
|
||||
state := &facadeDeliveryServer{t: t, commit: buildGit(t, repository, "rev-parse", "HEAD"), assets: make(map[string][]byte)}
|
||||
state := &facadeDeliveryServer{t: t, commit: buildGit(t, repository, "rev-parse", "HEAD"), tag: "v1.2.3", version: "v1.2.3", noteBody: "# Fixture v1.2.3\n\nDelivery fixture.\n", pages: make(map[string]bool), assets: make(map[string][]byte)}
|
||||
server := httptest.NewServer(http.HandlerFunc(state.serveHTTP))
|
||||
state.url = server.URL
|
||||
return server, state
|
||||
@@ -495,11 +608,11 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ
|
||||
state.t.Errorf("authorization = %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/repos/owner/example/tags/v1.2.3":
|
||||
_, _ = fmt.Fprintf(w, `{"name":"v1.2.3","commit":{"sha":%q}}`, state.commit)
|
||||
case "/api/v1/repos/owner/example/releases/tags/v1.2.3":
|
||||
if !state.page {
|
||||
switch r.URL.EscapedPath() {
|
||||
case "/api/v1/repos/owner/example/tags/" + url.PathEscape(state.tag):
|
||||
_, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, state.tag, state.commit)
|
||||
case "/api/v1/repos/owner/example/releases/tags/" + url.PathEscape(state.tag):
|
||||
if !state.pages[state.tag] {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
@@ -516,10 +629,10 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ
|
||||
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if payload.Tag != "v1.2.3" || payload.Target != state.commit || payload.Name != "Fixture v1.2.3" {
|
||||
if payload.Tag != state.tag || payload.Target != state.commit || payload.Name != "Fixture "+state.version {
|
||||
state.t.Errorf("release payload = %#v", payload)
|
||||
}
|
||||
state.page = true
|
||||
state.pages[state.tag] = true
|
||||
state.writeRelease(w)
|
||||
case "/api/v1/repos/owner/example/releases/10/assets":
|
||||
if r.Method == http.MethodGet {
|
||||
@@ -564,9 +677,9 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ
|
||||
func (state *facadeDeliveryServer) writeRelease(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": 10, "html_url": state.url + "/releases/10", "tag_name": "v1.2.3",
|
||||
"target_commitish": state.commit, "name": "Fixture v1.2.3",
|
||||
"body": "# Fixture v1.2.3\n\nDelivery fixture.\n", "draft": false, "prerelease": false,
|
||||
"id": 10, "html_url": state.url + "/releases/10", "tag_name": state.tag,
|
||||
"target_commitish": state.commit, "name": "Fixture " + state.version,
|
||||
"body": state.noteBody, "draft": false, "prerelease": false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,13 @@ The operation accepts historical lightweight and annotated tags, including a
|
||||
detached checkout, when the checked-out `HEAD` identifies the selected tag
|
||||
commit. It does not require the old remote `main` to match.
|
||||
|
||||
Delivery resolves the selected module's declaration and release note from the
|
||||
captured commit. `Version` remains the semantic version; the full tag includes
|
||||
the selected module's repository-relative physical path when it is nested. The
|
||||
profile or API request selects the physical module directory. The optional API
|
||||
`ExpectedTag` checks that a CI event's literal tag agrees with the derived tag
|
||||
before any build or hosted request.
|
||||
|
||||
## CI preparation
|
||||
|
||||
Use `PrepareTaggedCheckout` when a disposable CI checkout must be verified
|
||||
@@ -66,6 +73,8 @@ The CLI reads the named token variable only for `deliver`; its default name is
|
||||
creating or accepting a hosted release page. The full checked-in note is the
|
||||
hosted body. A retry accepts an identical page and matching attachments, adds
|
||||
only missing assets, and reports conflicts without replacing content.
|
||||
For nested modules, the note comes from that module's committed
|
||||
`docs/releases/<version>.md`, and the hosted page is keyed by its complete tag.
|
||||
|
||||
With no build targets, delivery is page-only: no builder, version smoke check,
|
||||
receipt, binary asset, or checksum manifest is involved. With selected targets,
|
||||
@@ -81,9 +90,10 @@ RELEASEKIT_GITEA_TOKEN="$GITEA_TOKEN" releasekit deliver --assets dist v1.2.3
|
||||
The build command does not need delivery credentials. The `--assets DIR`
|
||||
directory must contain the private `.releasekit-build.json` receipt created by
|
||||
`build`. ReleaseKit checks the receipt's repository, commit, version, resolved
|
||||
build settings, and file hashes before making a remote request; caller-owned
|
||||
files remain available for retries. Ordinary `deliver` does not download
|
||||
application dependencies or change the checkout.
|
||||
module directory and path, full tag, build settings, and file hashes before
|
||||
making a remote request; caller-owned files remain available for retries.
|
||||
Ordinary `deliver` does not download application dependencies or change the
|
||||
checkout.
|
||||
|
||||
The result reports `complete`, `partial`, `conflict`, `unknown`, or
|
||||
`not_attempted`, along with matching, uploaded, pending, conflicting, and
|
||||
@@ -110,19 +120,23 @@ outside the checkout and removed when delivery returns. `Output` receives build
|
||||
progress. With no build, or with an empty target set, delivery is page-only.
|
||||
|
||||
To deliver binaries built separately, set `Prebuilt` to the `BuildResult`.
|
||||
ReleaseKit checks that it identifies the selected repository, tagged commit, and
|
||||
version, that its source was verified clean and unchanged during compilation,
|
||||
and that the current files still match their recorded digests. The files remain
|
||||
caller-owned, so callers can retain the original bytes for retries and are
|
||||
responsible for cleanup. With neither artifact source, an empty managed target
|
||||
set, or an empty prebuilt result, delivery is page-only. Do not set both `Build`
|
||||
and `Prebuilt` in one request.
|
||||
ReleaseKit checks its repository, module directory, declared module path, full
|
||||
tag, semantic version, tagged commit, normalized settings, and artifact evidence
|
||||
against the selected committed module. Binary results must record verified
|
||||
source. The current files are checked against their recorded digests before any
|
||||
hosted request. The files remain caller-owned, so callers can retain the
|
||||
original bytes for retries and are responsible for cleanup. With neither
|
||||
artifact source, an empty managed target set, or a matching empty prebuilt
|
||||
result, delivery is page-only. Do not set both `Build` and `Prebuilt` in one
|
||||
request.
|
||||
|
||||
```go
|
||||
result, err := releasekit.Deliver(ctx, releasekit.DeliveryRequest{
|
||||
RepositoryDir: "/work/example",
|
||||
ModuleDir: "integrations/example",
|
||||
Product: "Example",
|
||||
Version: "v1.2.3",
|
||||
ExpectedTag: "integrations/example/v1.2.3", // optional CI event check
|
||||
Destination: releasekit.GiteaDestination{
|
||||
BaseURL: "https://gitea.example",
|
||||
Owner: "example",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Gitea release pages
|
||||
|
||||
ReleaseKit's public `Deliver` operation and `deliver` command verify tagged
|
||||
source, find or create its stable release page, inspect existing assets, and
|
||||
source, find or create the selected module's release page, inspect existing assets, and
|
||||
upload missing assets through the internal Gitea client. Page-only and
|
||||
binary-producing profiles use the same delivery path. The
|
||||
[consumer delivery guide](../consumers/delivery.md) covers caller workflows;
|
||||
@@ -13,16 +13,19 @@ the `Authorization: token …` request header and is not included in errors or
|
||||
results. The base URL must use HTTP or HTTPS and cannot contain URL credentials,
|
||||
a query, or a fragment.
|
||||
|
||||
For version `vMAJOR.MINOR.PATCH`, the client first reads
|
||||
The selected module derives a complete tag from its semantic version and
|
||||
physical repository-relative directory. For example, version `v1.2.3` in
|
||||
`integrations/ckconfig` uses `integrations/ckconfig/v1.2.3`; a root module uses
|
||||
`v1.2.3`. The client first reads
|
||||
`/api/v1/repos/{owner}/{repo}/tags/{tag}` and requires the hosted tag's commit
|
||||
SHA to equal the captured source commit. It then reads
|
||||
`/api/v1/repos/{owner}/{repo}/releases/tags/{tag}`. A matching existing page
|
||||
must have that tag, the complete committed release note as its body, and stable
|
||||
non-draft state. Its existing title is retained. ReleaseKit does not edit a
|
||||
conflicting page.
|
||||
conflicting page. Slash-containing tags are escaped as one URL path segment.
|
||||
|
||||
If the page is absent, the client posts to `/api/v1/repos/{owner}/{repo}/releases`
|
||||
with the selected tag and commit, the release note's H1 as title, the complete
|
||||
with the selected full tag and commit, the release note's H1 as title, the complete
|
||||
note as body, and draft and prerelease disabled. A create conflict, interrupted
|
||||
response, or malformed create response (including inconsistent decoded fields)
|
||||
triggers one lookup. An exact page found
|
||||
|
||||
@@ -39,9 +39,9 @@ reports these implemented packages:
|
||||
| `internal/artifact` | Shared build-target normalization, artifact declarations, filename rules, and checksum-manifest naming. |
|
||||
| `internal/build` | Resolved-identity builds, normalized settings, selected-module compilation, version injection, external native smoke output, and artifact digests. |
|
||||
| `internal/candidate` | Stable-version, note, repository-state, candidate capture, and post-validation checks. |
|
||||
| `internal/delivery` | Tagged-source and release-note checks, managed builds in disposable storage outside the checkout, structural artifact declaration, manifest assembly, one local digest-and-size preflight before hosted requests, and delivery coordination. |
|
||||
| `internal/git` | Git command boundary, committed-tree reads, lazy local-only batched blob reads, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. |
|
||||
| `internal/gitea` | Gitea API transport, hosted release pages, asset inspection, manifests, and uploads. |
|
||||
| `internal/delivery` | Captured-commit module identity and note selection, literal-tag checkout checks, prebuilt identity binding, managed builds in disposable storage outside the checkout, structural artifact declaration, manifest assembly, one local digest-and-size preflight before hosted requests, and delivery coordination. |
|
||||
| `internal/git` | Git command boundary, committed-tree reads, checkout-commit capture, lazy local-only batched blob reads, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. |
|
||||
| `internal/gitea` | Gitea API transport, full-tag hosted release pages, asset inspection, manifests, and uploads. |
|
||||
| `internal/gotool` | Operation-local Go configuration discovery and reuse, shared native-validation and static-build environments, and network-capable dependency preparation; see [Go command environments](gotool.md). |
|
||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||
| `internal/process` | Cancellable child-process execution, including owned stdin/stdout pipes for serialized exchanges, with per-command environment overlays and removals. |
|
||||
@@ -88,7 +88,7 @@ not a separate release matrix:
|
||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishRejectsUnexpectedPushURLBeforeValidation`, `TestPublishComparesExpectedURLWithResolvedInsteadOfDestination`, `TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer`, `TestCheckIgnoresExpectedPushURL`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
||||
| Cross-builds, version smoke checks, module paths, empty target sets, and partial outputs | `releasekit.Build`, `internal/build`, `internal/artifact`, `internal/tempdir`, `buildinfo` | `TestBuildCreatesSelectedTargetsAndInjectsVersion`, `TestBuildUsesNestedWorkingTreeModuleAndModuleRelativeOutput`, `TestBuildResolvesDirtyWorkingTreeDeclaration`, `TestBuildWithNoTargetsDoesNotInvokeCompiler`, `TestBuildReturnsPartialArtifactsAfterTargetFailure`, `TestNormalizeSettingsKeepsBuildInputsAndNormalizesTargets`, `TestOutsideFallsBackWhenTemporaryDirectoryIsInsideWorktree` |
|
||||
| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` |
|
||||
| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverUsesCommittedModuleIdentityForSeparatePages`, `TestDeliverRejectsPrebuiltArtifactsForAnotherModuleBeforeHostedRequests`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites`, `TestEnsureReleaseEscapesSlashContainingTag` |
|
||||
| Tagged checkout and build dependency preparation | `releasekit.PrepareTaggedCheckout`, `releasekit.PrepareBuildDependencies`, `internal/preparation`, `internal/git`, `internal/gotool` | `TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches`, `TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout`, `TestPrepareTaggedCheckoutReportsLocalCheckoutConflict`, `TestPrepareTaggedCheckoutReportsConflictingLocalTag`, `TestPrepareBuildDependenciesPreservesPreparationEnvironment`, `TestPrepareBuildDependenciesPropagatesCommandFailure`, `TestPrepareBuildDependenciesPropagatesCancellation` |
|
||||
| Page, manifest, asset comparison, and additive retry outcomes | `internal/gitea` | `TestEnsureReleaseCreatesMissingPageFromCommittedNote`, `TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes`, `TestUploadMissingPartialFailureThenRerunOnlyMissingAssets` |
|
||||
| CLI profiles and prepared Woodpecker sequences | `cmd/releasekit`, `cmd/releasekit/internal/cli`, `examples/woodpecker/` | `TestLoadSettingsRejectsInvalidProfiles`, `TestWoodpeckerTagDeliveryTemplate`, `TestWoodpeckerTagDeliveryUsesTaggedModuleVersion`, `TestWoodpeckerSourceDeliveryTemplateSelfBootstraps`, `TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout` |
|
||||
|
||||
@@ -459,6 +459,17 @@ separate pages. Cover nested managed builds and reject an API prebuilt result
|
||||
for another module before hosted requests. Reuse established tampering, conflict,
|
||||
partial/unknown, historical-tag, and cleanup tests. Run delivery/Gitea tests.
|
||||
|
||||
**Completed:** Delivery now captures checkout identity once, derives the
|
||||
selected module's committed tag and note path, compares an optional expected
|
||||
full tag, and returns module evidence through local and hosted failures. Managed
|
||||
and prebuilt binary results bind to the same repository, module, version, tag,
|
||||
commit, settings, and source verification; the CLI carries that identity through
|
||||
its existing private receipt. Gitea uses the literal full tag and escapes
|
||||
slashes in hosted routes. Verified two module pages at one commit, nested notes
|
||||
and slash tags, mismatched expected tags and prebuilt identities, preserved
|
||||
delivery recovery cases, `go test ./...`, `go run ./internal/apicheck`, and
|
||||
`git diff --check`.
|
||||
|
||||
## Stage 9: Replace receipts with the module-bound schema
|
||||
|
||||
**Outcome:** CLI prebuilt delivery uses one current receipt format and one
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||
@@ -33,6 +34,7 @@ type Request struct {
|
||||
ModuleDir string
|
||||
Product string
|
||||
Version string
|
||||
ExpectedTag string
|
||||
Destination gitea.Destination
|
||||
Build *internalbuild.Spec
|
||||
Prebuilt *internalbuild.Result
|
||||
@@ -69,36 +71,51 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("resolve repository directory: %w", err)
|
||||
}
|
||||
source, err := git.InspectTaggedSource(ctx, repositoryDir, request.Version)
|
||||
result.RepositoryDir = repositoryDir
|
||||
selection, err := releaseidentity.ResolveSelection(repositoryDir, request.ModuleDir)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("inspect tagged source: %w", err)
|
||||
return result, fmt.Errorf("resolve delivery module: %w", err)
|
||||
}
|
||||
result.CandidateCommit = source.TagCommit
|
||||
if source.Status != "" {
|
||||
return result, fmt.Errorf("tagged checkout must be clean:\n%s", source.Status)
|
||||
}
|
||||
if source.Head != source.TagCommit {
|
||||
return result, fmt.Errorf("checkout HEAD %s does not match tag %s commit %s", source.Head, request.Version, source.TagCommit)
|
||||
}
|
||||
notePath := "docs/releases/" + request.Version + ".md"
|
||||
note, err := git.ReadCommitFile(ctx, repositoryDir, source.TagCommit, notePath)
|
||||
result.ModuleDir = selection.ModuleDir
|
||||
commit, err := git.CaptureHead(ctx, repositoryDir)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.CandidateCommit = commit
|
||||
identity, err := releaseidentity.FromCommittedSelection(ctx, selection, request.Version, commit)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("resolve committed delivery identity: %w", err)
|
||||
}
|
||||
result.RepositoryDir = identity.RepositoryDir
|
||||
result.ModuleDir = identity.ModuleDir
|
||||
result.ModulePath = identity.ModulePath
|
||||
result.Tag = identity.Tag
|
||||
if request.ExpectedTag != "" && request.ExpectedTag != identity.Tag {
|
||||
return result, fmt.Errorf("expected delivery tag %q does not match selected module tag %q", request.ExpectedTag, identity.Tag)
|
||||
}
|
||||
source, err := git.InspectTaggedSource(ctx, repositoryDir, identity.Tag)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("inspect tagged source: %w", err)
|
||||
}
|
||||
if source.Status != "" {
|
||||
return result, fmt.Errorf("tagged checkout must be clean:\n%s", source.Status)
|
||||
}
|
||||
if source.Head != commit {
|
||||
return result, fmt.Errorf("checkout HEAD changed from captured commit %s to %s", commit, source.Head)
|
||||
}
|
||||
if source.TagCommit != commit {
|
||||
return result, fmt.Errorf("checkout HEAD %s does not match tag %s commit %s", source.Head, identity.Tag, source.TagCommit)
|
||||
}
|
||||
note, err := git.ReadCommitFile(ctx, repositoryDir, commit, identity.NotePath)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("read release note %q: %w", identity.NotePath, err)
|
||||
}
|
||||
if err := candidate.ValidateReleaseNote(note, request.Product, request.Version); err != nil {
|
||||
return result, fmt.Errorf("release note %q: %w", notePath, err)
|
||||
return result, fmt.Errorf("release note %q: %w", identity.NotePath, err)
|
||||
}
|
||||
|
||||
build := request.Prebuilt
|
||||
if request.Build != nil && len(request.Build.Targets) != 0 {
|
||||
selection, err := releaseidentity.ResolveSelection(repositoryDir, request.ModuleDir)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("resolve managed build module: %w", err)
|
||||
}
|
||||
identity, err := releaseidentity.FromCommittedSelection(ctx, selection, request.Version, source.TagCommit)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("resolve managed build identity: %w", err)
|
||||
}
|
||||
temporaryDir, err := tempdir.Outside(repositoryDir, "releasekit-delivery-build-*")
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("create temporary delivery build directory: %w", err)
|
||||
@@ -109,6 +126,7 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
||||
Spec: *request.Build,
|
||||
OutputDir: filepath.Join(temporaryDir, "assets"),
|
||||
Output: request.Output,
|
||||
ToolOwner: gotool.NewOwner(identity.ModuleDir),
|
||||
})
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("build delivery assets: %w", err)
|
||||
@@ -122,7 +140,7 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
if err := validateBuild(repositoryDir, source.TagCommit, request.Version, build, assets); err != nil {
|
||||
if err := validateBuild(identity, commit, build, assets); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if len(assets) > 0 {
|
||||
@@ -131,22 +149,22 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
||||
}
|
||||
}
|
||||
if build != nil && len(build.Settings.Targets) != 0 {
|
||||
current, err := git.InspectTaggedSource(ctx, repositoryDir, request.Version)
|
||||
current, err := git.InspectTaggedSource(ctx, repositoryDir, identity.Tag)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("recheck tagged source after build: %w", err)
|
||||
}
|
||||
if current.Status != "" {
|
||||
return result, fmt.Errorf("tagged checkout changed during build:\n%s", current.Status)
|
||||
}
|
||||
if current.Head != source.TagCommit || current.TagCommit != source.TagCommit {
|
||||
if current.Head != commit || current.TagCommit != commit {
|
||||
return result, errors.New("tagged source changed during build")
|
||||
}
|
||||
}
|
||||
|
||||
releaseResult, err := (gitea.Client{}).EnsureRelease(ctx, gitea.Request{
|
||||
Destination: request.Destination,
|
||||
Version: request.Version,
|
||||
Commit: source.TagCommit,
|
||||
Tag: identity.Tag,
|
||||
Commit: commit,
|
||||
Note: string(note),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -180,13 +198,24 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
||||
return result, uploadErr
|
||||
}
|
||||
|
||||
func validateBuild(repositoryDir, commit, version string, build *internalbuild.Result, assets []gitea.ExpectedAsset) error {
|
||||
func validateBuild(identity releaseidentity.Identity, commit string, build *internalbuild.Result, assets []gitea.ExpectedAsset) error {
|
||||
if build == nil {
|
||||
if len(assets) != 0 {
|
||||
return errors.New("assets require a build identity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buildRepository, err := filepath.Abs(build.RepositoryDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve build repository: %w", err)
|
||||
}
|
||||
buildModule, err := filepath.Abs(build.ModuleDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve build module: %w", err)
|
||||
}
|
||||
if buildRepository != identity.RepositoryDir || buildModule != identity.ModuleDir || build.ModulePath != identity.ModulePath || build.Tag != identity.Tag || build.CandidateCommit != commit || build.Version != identity.Version {
|
||||
return errors.New("build result does not identify the selected tagged module")
|
||||
}
|
||||
if len(build.Settings.Targets) == 0 {
|
||||
if len(assets) != 0 {
|
||||
return errors.New("page-only delivery cannot include binary assets")
|
||||
@@ -196,13 +225,6 @@ func validateBuild(repositoryDir, commit, version string, build *internalbuild.R
|
||||
if !build.SourceVerified {
|
||||
return errors.New("build source was not verified as clean and unchanged during compilation")
|
||||
}
|
||||
buildRepository, err := filepath.Abs(build.RepositoryDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve build repository: %w", err)
|
||||
}
|
||||
if buildRepository != repositoryDir || build.CandidateCommit != commit || build.Version != version {
|
||||
return errors.New("build result does not identify the selected tagged source")
|
||||
}
|
||||
if strings.TrimSpace(build.Settings.MainPackage) == "" || strings.TrimSpace(build.Settings.BinaryName) == "" || len(assets) != len(build.Settings.Targets)+1 || assets[len(assets)-1].Filename != gitea.ManifestFilename {
|
||||
return errors.New("build result does not contain the selected artifacts and complete checksum manifest")
|
||||
}
|
||||
|
||||
@@ -30,6 +30,20 @@ type TaggedSource struct {
|
||||
TagObjectType string
|
||||
}
|
||||
|
||||
// CaptureHead records the checkout commit before identity derived from files
|
||||
// in that commit is inspected.
|
||||
func CaptureHead(ctx context.Context, repositoryDir string) (string, error) {
|
||||
head, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", "HEAD^{commit}")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read checkout HEAD: %w", err)
|
||||
}
|
||||
head = strings.TrimSpace(head)
|
||||
if !validObjectID(head) {
|
||||
return "", fmt.Errorf("Git returned an invalid checkout commit %q", head)
|
||||
}
|
||||
return head, nil
|
||||
}
|
||||
|
||||
// InspectTaggedSource reads the local HEAD, worktree state, and selected tag.
|
||||
// Both lightweight historical tags and annotated tags are supported.
|
||||
func InspectTaggedSource(ctx context.Context, repositoryDir, tag string) (TaggedSource, error) {
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||
)
|
||||
|
||||
type FailureKind string
|
||||
@@ -59,7 +57,7 @@ type Destination struct {
|
||||
|
||||
type Request struct {
|
||||
Destination Destination
|
||||
Version string
|
||||
Tag string
|
||||
Commit string
|
||||
Note string
|
||||
}
|
||||
@@ -122,17 +120,17 @@ func (c Client) EnsureRelease(ctx context.Context, request Request) (Result, err
|
||||
}
|
||||
client := c.httpClient()
|
||||
apiRoot := strings.TrimRight(base.String(), "/") + "/api/v1/repos/" + url.PathEscape(request.Destination.Owner) + "/" + url.PathEscape(request.Destination.Repository)
|
||||
tagURL := apiRoot + "/tags/" + url.PathEscape(request.Version)
|
||||
tagURL := apiRoot + "/tags/" + url.PathEscape(request.Tag)
|
||||
|
||||
var tag tagResponse
|
||||
if err := c.getJSON(ctx, client, request.Destination.Token, "get hosted tag", tagURL, &tag); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if tag.Commit.SHA != request.Commit {
|
||||
return Result{}, failure(FailureConflict, "verify hosted tag", 0, fmt.Errorf("tag %q targets %q, want %q", request.Version, tag.Commit.SHA, request.Commit))
|
||||
return Result{}, failure(FailureConflict, "verify hosted tag", 0, fmt.Errorf("tag %q targets %q, want %q", request.Tag, tag.Commit.SHA, request.Commit))
|
||||
}
|
||||
|
||||
releaseURL := apiRoot + "/releases/tags/" + url.PathEscape(request.Version)
|
||||
releaseURL := apiRoot + "/releases/tags/" + url.PathEscape(request.Tag)
|
||||
existing, status, err := c.getRelease(ctx, client, request.Destination.Token, releaseURL)
|
||||
if err == nil {
|
||||
return acceptExisting(existing, request)
|
||||
@@ -143,7 +141,7 @@ func (c Client) EnsureRelease(ctx context.Context, request Request) (Result, err
|
||||
|
||||
title := noteTitle(request.Note)
|
||||
body, err := json.Marshal(createReleaseRequest{
|
||||
TagName: request.Version, TargetCommitish: request.Commit,
|
||||
TagName: request.Tag, TargetCommitish: request.Commit,
|
||||
Name: title, Body: request.Note,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -272,8 +270,8 @@ func (c Client) httpClient() *http.Client {
|
||||
}
|
||||
|
||||
func validateRequest(request Request) error {
|
||||
if !versioncontract.ValidStableVersion(request.Version) {
|
||||
return failure(FailureInvalid, "validate request", 0, errors.New("stable version is required"))
|
||||
if strings.TrimSpace(request.Tag) == "" {
|
||||
return failure(FailureInvalid, "validate request", 0, errors.New("full release tag is required"))
|
||||
}
|
||||
if !validObjectID(request.Commit) {
|
||||
return failure(FailureInvalid, "validate request", 0, errors.New("source commit must be a lowercase Git object ID"))
|
||||
@@ -305,14 +303,14 @@ func parseBaseURL(raw string) (*url.URL, error) {
|
||||
}
|
||||
|
||||
func acceptExisting(release Release, request Request) (Result, error) {
|
||||
if release.TagName != request.Version || release.Body != request.Note || release.Draft || release.Prerelease {
|
||||
if release.TagName != request.Tag || release.Body != request.Note || release.Draft || release.Prerelease {
|
||||
return Result{}, failure(FailureConflict, "verify existing release", 0, errors.New("existing release does not match the committed note and stable tag"))
|
||||
}
|
||||
return Result{Release: release}, nil
|
||||
}
|
||||
|
||||
func acceptCreated(release Release, request Request) (Result, error) {
|
||||
if release.ID <= 0 || release.TagName != request.Version || release.Body != request.Note || release.Title != noteTitle(request.Note) || release.Draft || release.Prerelease {
|
||||
if release.ID <= 0 || release.TagName != request.Tag || release.Body != request.Note || release.Title != noteTitle(request.Note) || release.Draft || release.Prerelease {
|
||||
return Result{}, failure(FailureMalformed, "verify created release", 0, errors.New("created release response does not match the requested stable release"))
|
||||
}
|
||||
return Result{Release: release, Created: true}, nil
|
||||
|
||||
@@ -26,6 +26,45 @@ func TestEnsureReleaseFindsMatchingReleaseAndPreservesTitle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureReleaseEscapesSlashContainingTag(t *testing.T) {
|
||||
tag := "integrations/ckconfig/v1.2.3"
|
||||
state := &fixtureState{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertAuth(t, r)
|
||||
switch r.URL.EscapedPath() {
|
||||
case "/api/v1/repos/owner/project/tags/integrations%2Fckconfig%2Fv1.2.3":
|
||||
assertRequest(t, r, http.MethodGet, r.URL.Path)
|
||||
writeTag(t, w, testCommit)
|
||||
case "/api/v1/repos/owner/project/releases/tags/integrations%2Fckconfig%2Fv1.2.3":
|
||||
assertRequest(t, r, http.MethodGet, r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
case "/api/v1/repos/owner/project/releases":
|
||||
assertRequest(t, r, http.MethodPost, r.URL.Path)
|
||||
state.posts++
|
||||
var payload createReleaseRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload.TagName != tag || payload.Name != "Example v1.2.3" || payload.Body != testNote {
|
||||
t.Fatalf("create payload = %#v", payload)
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(releaseResponse{ID: 7, HTMLURL: "https://example.test/release", TagName: tag, TargetCommitish: testCommit, Name: payload.Name, Body: payload.Body}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected escaped request %s %s", r.Method, r.URL.EscapedPath())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
request := testRequest(server.URL)
|
||||
request.Tag = tag
|
||||
result, err := testClient(server).EnsureRelease(context.Background(), request)
|
||||
if err != nil || !result.Created || state.posts != 1 {
|
||||
t.Fatalf("nested tag release = %#v, posts %d, error %v", result, state.posts, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureReleaseCreatesMissingPageFromCommittedNote(t *testing.T) {
|
||||
server, _ := newReleaseServer(t, releaseFixture{}, true)
|
||||
result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
|
||||
@@ -324,7 +363,7 @@ func testClient(server *httptest.Server) Client {
|
||||
}
|
||||
|
||||
func testRequest(baseURL string) Request {
|
||||
return Request{Destination: Destination{BaseURL: baseURL, Owner: "owner", Repository: "project", Token: testToken}, Version: "v1.2.3", Commit: testCommit, Note: testNote}
|
||||
return Request{Destination: Destination{BaseURL: baseURL, Owner: "owner", Repository: "project", Token: testToken}, Tag: "v1.2.3", Commit: testCommit, Note: testNote}
|
||||
}
|
||||
|
||||
func failureKind(t *testing.T, err error) FailureKind {
|
||||
|
||||
Reference in New Issue
Block a user