feat: publish releases with this repository's own SDK code
There is no CI: the instance has no act_runner and none is planned, so releases are cut by hand. That makes `make check` the only thing standing between a mistake and the tracker, and it is one command: gofmt, vet, the suite with the cache defeated, `go mod verify`, a vendored build, and `kettle gen skills --check`. The last one is the invariant worth having — the plugin's SKILL.md command reference is generated from the binary's registry, so a flag that changed cannot ship with documentation that recommends the old one. `cli/cmd/release` publishes to Gitea using the same SDK the binary already vendors, which is a pleasing thing to be able to say: nothing third-party handles the artifacts. It is a second binary rather than a `kettle` subcommand on purpose — `kettle`'s command tree is what generates the plugin's skills, so a verb there ships to every operator, and publishing a release is build infrastructure. It is idempotent end to end: an existing release for the tag is reused, an asset of the same name is replaced rather than doubled, and a retried run converges instead of duplicating. `make release` refuses three things, each with its own message: a dirty working tree, a TAG that is not what `git describe` reports, and a tag the remote does not have. A release built from uncommitted code is unreproducible and nobody finds out until they need to reproduce it. `kettle version` reports the stamp, the toolchain and the VCS revision. The default is `dev`, and a hand build says so and means it — a binary out of somebody's working tree is not a release and must not claim to be one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -243,7 +243,34 @@ func Resolve(start string) (*Resolved, error) {
|
||||
} else if !errors.Is(err, ErrNoConfig) {
|
||||
return nil, err
|
||||
}
|
||||
return merge(p)
|
||||
}
|
||||
|
||||
// ResolveOutsideAProject is Resolve for a caller that legitimately has no
|
||||
// project to stand in.
|
||||
//
|
||||
// `cmd/release` is the one, and it is not an exception being carved out: the
|
||||
// marker is gitignored, so a fresh clone has none, and a tool that publishes a
|
||||
// tag must not create one on its way past. With no marker there is nothing to
|
||||
// merge and the ENVIRONMENT IS the configuration — KETTLE_URL, KETTLE_TOKEN and
|
||||
// KETTLE_REPO, which is exactly what somebody exports before cutting a release.
|
||||
//
|
||||
// A marker that IS there is read as always, overrides and all, so the same
|
||||
// command run from a maintainer's own checkout picks up the login pinned in it
|
||||
// and needs no token in the shell.
|
||||
//
|
||||
// Every other caller wants Resolve: for `kettle`, "no project" is the answer,
|
||||
// not a state to work around. A push that quietly ran against whatever was in
|
||||
// the environment would be a push into somebody else's repository.
|
||||
func ResolveOutsideAProject(start string) (*Resolved, error) {
|
||||
if ProjectPath(start) == "" {
|
||||
return merge(Project{})
|
||||
}
|
||||
return Resolve(start)
|
||||
}
|
||||
|
||||
// merge applies the login file and the environment to a project's settings.
|
||||
func merge(p Project) (*Resolved, error) {
|
||||
out := &Resolved{Login: p.Login}
|
||||
if v := os.Getenv(EnvLogin); v != "" {
|
||||
out.Login = v
|
||||
@@ -295,6 +322,21 @@ func Require(start string) (*Resolved, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.Complete(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Complete reports what a resolved configuration is still missing, naming the
|
||||
// one command or the one variable that supplies each.
|
||||
//
|
||||
// A half-filled struct allowed through is a 401 three calls later, and "401
|
||||
// Unauthorized" names nothing an operator can act on. It is a method rather
|
||||
// than part of Resolve because the two questions are different: `kettle config`
|
||||
// wants to SHOW a half-filled configuration, and everything that dials wants to
|
||||
// refuse one.
|
||||
func (r *Resolved) Complete() error {
|
||||
var missing []string
|
||||
if r.URL == "" {
|
||||
missing = append(missing, "a URL (pin a login with `kettle init --login`, or set "+EnvURL+")")
|
||||
@@ -306,9 +348,9 @@ func Require(start string) (*Resolved, error) {
|
||||
missing = append(missing, "a repository (`kettle init --repo owner/name`, or set "+EnvRepo+")")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return nil, fmt.Errorf("this project has no %s", strings.Join(missing, ", and no "))
|
||||
return fmt.Errorf("this project has no %s", strings.Join(missing, ", and no "))
|
||||
}
|
||||
return r, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// strictUnmarshal refuses keys the struct does not know.
|
||||
|
||||
Reference in New Issue
Block a user