182 lines
5.5 KiB
Go
182 lines
5.5 KiB
Go
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
|
|
SourceVerified bool
|
|
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.WriteAttempted {
|
|
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
|
|
}
|
|
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.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
|
|
}
|