Expose tagged-source delivery through facade and CLI
This commit is contained in:
177
internal/delivery/delivery.go
Normal file
177
internal/delivery/delivery.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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/versioncontract"
|
||||
)
|
||||
|
||||
type State string
|
||||
|
||||
const (
|
||||
NotAttempted State = "not_attempted"
|
||||
Complete State = "complete"
|
||||
Partial State = "partial"
|
||||
Conflict State = "conflict"
|
||||
Unknown State = "unknown"
|
||||
)
|
||||
|
||||
type BuildIdentity struct {
|
||||
RepositoryDir string
|
||||
CandidateCommit string
|
||||
Version string
|
||||
MainPackage string
|
||||
BinaryName string
|
||||
Targets []string
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
RepositoryDir string
|
||||
Product string
|
||||
Version string
|
||||
Destination gitea.Destination
|
||||
Build *BuildIdentity
|
||||
Assets []gitea.ExpectedAsset
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
CandidateCommit string
|
||||
ReleaseURL string
|
||||
State State
|
||||
Matching []gitea.AssetIdentity
|
||||
Uploaded []gitea.AssetIdentity
|
||||
Pending []gitea.AssetIdentity
|
||||
Conflicting []gitea.AssetIdentity
|
||||
Uncertain []gitea.AssetIdentity
|
||||
}
|
||||
|
||||
func Deliver(ctx context.Context, request Request) (Result, error) {
|
||||
result := Result{State: NotAttempted}
|
||||
if !versioncontract.ValidStableVersion(request.Version) {
|
||||
return result, candidate.ErrInvalidVersion
|
||||
}
|
||||
if strings.TrimSpace(request.Product) == "" {
|
||||
return result, errors.New("product is required")
|
||||
}
|
||||
repositoryDir, err := filepath.Abs(request.RepositoryDir)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("resolve repository directory: %w", err)
|
||||
}
|
||||
source, err := git.InspectTaggedSource(ctx, repositoryDir, request.Version)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("inspect tagged source: %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)
|
||||
}
|
||||
if err := validateBuild(repositoryDir, source.TagCommit, request.Version, request.Build, request.Assets); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if len(request.Assets) > 0 {
|
||||
if err := gitea.VerifyLocalAssets(request.Assets); err != nil {
|
||||
return result, fmt.Errorf("verify build assets: %w", err)
|
||||
}
|
||||
}
|
||||
notePath := "docs/releases/" + request.Version + ".md"
|
||||
note, err := git.ReadCommitFile(ctx, repositoryDir, source.TagCommit, notePath)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if err := candidate.ValidateReleaseNote(note, request.Product, request.Version); err != nil {
|
||||
return result, fmt.Errorf("release note %q: %w", notePath, err)
|
||||
}
|
||||
|
||||
releaseResult, err := (gitea.Client{}).EnsureRelease(ctx, gitea.Request{
|
||||
Destination: request.Destination,
|
||||
Version: request.Version,
|
||||
Commit: source.TagCommit,
|
||||
Note: string(note),
|
||||
})
|
||||
if err != nil {
|
||||
result.State = stateForError(err, false)
|
||||
return result, err
|
||||
}
|
||||
result.ReleaseURL = releaseResult.Release.URL
|
||||
if len(request.Assets) == 0 {
|
||||
result.State = Complete
|
||||
return result, nil
|
||||
}
|
||||
inspection, err := (gitea.Client{}).InspectAssets(ctx, request.Destination, releaseResult.Release.ID, request.Assets)
|
||||
if err != nil {
|
||||
result.State = NotAttempted
|
||||
if releaseResult.Created {
|
||||
result.State = Partial
|
||||
}
|
||||
result.Uncertain = assetIdentities(request.Assets)
|
||||
return result, err
|
||||
}
|
||||
uploadResult, uploadErr := (gitea.Client{}).UploadMissing(ctx, request.Destination, releaseResult.Release.ID, inspection)
|
||||
result.Matching = uploadResult.Matching
|
||||
result.Uploaded = uploadResult.Uploaded
|
||||
result.Pending = uploadResult.Pending
|
||||
result.Conflicting = uploadResult.Conflicting
|
||||
result.Uncertain = uploadResult.Uncertain
|
||||
result.State = State(uploadResult.State)
|
||||
return result, uploadErr
|
||||
}
|
||||
|
||||
func validateBuild(repositoryDir, commit, version string, build *BuildIdentity, assets []gitea.ExpectedAsset) error {
|
||||
if build == nil {
|
||||
if len(assets) != 0 {
|
||||
return errors.New("assets require a build identity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(build.Targets) == 0 {
|
||||
if len(assets) != 0 {
|
||||
return errors.New("page-only delivery cannot include binary assets")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
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.MainPackage) == "" || strings.TrimSpace(build.BinaryName) == "" || len(assets) != len(build.Targets)+1 || assets[len(assets)-1].Filename != gitea.ManifestFilename {
|
||||
return errors.New("build result does not contain the selected artifacts and complete checksum manifest")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stateForError(err error, pageCreated bool) State {
|
||||
var failure *gitea.Failure
|
||||
if errors.As(err, &failure) {
|
||||
switch failure.Kind {
|
||||
case gitea.FailureConflict:
|
||||
return Conflict
|
||||
case gitea.FailureUnknown:
|
||||
return Unknown
|
||||
}
|
||||
}
|
||||
if pageCreated {
|
||||
return Partial
|
||||
}
|
||||
return NotAttempted
|
||||
}
|
||||
|
||||
func assetIdentities(assets []gitea.ExpectedAsset) []gitea.AssetIdentity {
|
||||
result := make([]gitea.AssetIdentity, 0, len(assets))
|
||||
for _, asset := range assets {
|
||||
result = append(result, gitea.AssetIdentity{Filename: asset.Filename, SHA256: asset.SHA256})
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user