mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-12 11:01:24 -07:00
661480a56d
- add internal/version: resolve from the release ldflag, else the go build info (module tag / vcs revision), else "dev" - show the version on the boot banner and for `sif version` - Makefile now stamps `make` builds via git describe (matching the release ci), so local/go-install builds report a real version instead of "dev" - patchnotes.ShowOnce skips pseudo/dev versions so non-release builds dont make a doomed github call - document sif version / sif patchnote / SIF_NO_PATCHNOTES in the readme + usage
146 lines
4.3 KiB
Go
146 lines
4.3 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
// Package patchnotes shows release notes pulled from the github releases.
|
|
package patchnotes
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/glamour"
|
|
)
|
|
|
|
const releasesAPI = "https://api.github.com/repos/vmfunc/sif/releases"
|
|
|
|
type release struct {
|
|
TagName string `json:"tag_name"`
|
|
Name string `json:"name"`
|
|
Body string `json:"body"`
|
|
URL string `json:"html_url"`
|
|
}
|
|
|
|
func fetch(ctx context.Context, path string) (*release, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releasesAPI+path, http.NoBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("github returned %s", resp.Status)
|
|
}
|
|
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var r release
|
|
if err := json.Unmarshal(body, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// render turns a release's markdown body into styled terminal output, falling
|
|
// back to the raw body if glamour can't render it.
|
|
func render(r *release) string {
|
|
out, err := glamour.Render(r.Body, "dark")
|
|
if err != nil {
|
|
return r.Body
|
|
}
|
|
return fmt.Sprintf("%s\n%s", r.TagName, out)
|
|
}
|
|
|
|
// Print fetches the latest release and writes its notes to stdout. tag may be
|
|
// empty for the latest release, or a "vX" tag for a specific one.
|
|
func Print(tag string) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
path := "/latest"
|
|
if tag != "" {
|
|
path = "/tags/" + tag
|
|
}
|
|
|
|
r, err := fetch(ctx, path)
|
|
if err != nil {
|
|
fmt.Printf("couldn't fetch patch notes: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Print(render(r))
|
|
}
|
|
|
|
// ShowOnce prints the running version's notes the first time that version runs,
|
|
// then records it so it isn't shown again. best-effort: dev builds, the
|
|
// SIF_NO_PATCHNOTES opt-out, and any network failure stay silent.
|
|
func ShowOnce(version string) {
|
|
// only clean release tags (e.g. 2026.6.7) map to a github release; skip dev
|
|
// and pseudo-versions (a commit/dirty build) so we don't make a doomed call.
|
|
if version == "" || version == "dev" || strings.ContainsAny(version, "-+") || os.Getenv("SIF_NO_PATCHNOTES") != "" {
|
|
return
|
|
}
|
|
|
|
path, err := statePath()
|
|
if err != nil || hasSeen(path, version) {
|
|
return
|
|
}
|
|
// record before fetching so a flaky network doesn't nag on every run
|
|
recordSeen(path, version)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
r, err := fetch(ctx, "/tags/v"+version)
|
|
if err != nil {
|
|
return
|
|
}
|
|
fmt.Printf("\nwhat's new in this release:\n%s", render(r))
|
|
}
|
|
|
|
func statePath() (string, error) {
|
|
dir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(dir, "sif", "seen_version"), nil
|
|
}
|
|
|
|
func hasSeen(path, version string) bool {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return strings.TrimSpace(string(data)) == version
|
|
}
|
|
|
|
func recordSeen(path, version string) {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
|
return
|
|
}
|
|
_ = os.WriteFile(path, []byte(version), 0o600)
|
|
}
|