Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ More end-to-end recipes → **[docs/EXAMPLES.md](docs/EXAMPLES.md)**
| `mask` | Hide secrets in logs (regex patterns, partial reveal, GitHub `::add-mask::`) | [↗](docs/COMMANDS.md#mask) |
| `transform` | base64, URL-encode, case conversion, regex replace, templates, hash, slug | [↗](docs/COMMANDS.md#transform) |
| `summary` | Append markdown / tables / badges / collapsibles to `$GITHUB_STEP_SUMMARY` | [↗](docs/COMMANDS.md#summary) |
| `assert` | Pipeline guards: env vars, files, JSON paths, semver, URL health | [↗](docs/COMMANDS.md#assert) |
| `assert` | Pipeline guards: env vars, files, JSON paths, semver, URL health, version sync | [↗](docs/COMMANDS.md#assert) |
| `matrix` | Build GitHub Actions matrix JSON from dirs / files / JSON / Cartesian product | [↗](docs/COMMANDS.md#matrix) |
| `notify` | Slack / Discord / Teams / generic webhook with status formatting | [↗](docs/COMMANDS.md#notify) |
| `wait` | Poll a URL / TCP port / shell command until ready | [↗](docs/COMMANDS.md#wait) |
Expand All @@ -86,22 +86,25 @@ More end-to-end recipes → **[docs/EXAMPLES.md](docs/EXAMPLES.md)**
| `retry` | Run any command with attempt count, delay, backoff, exit-code filtering | [↗](docs/COMMANDS.md#retry) |
| `cache-key` | Deterministic SHA256 cache keys from files / globs / composite parts | [↗](docs/COMMANDS.md#cache-key) |
| `checksum` | Generate / verify release checksums for artifact files | [↗](docs/COMMANDS.md#checksum) |
| `artifact` | Assert artifacts exist and generate size/SHA256 manifests | [↗](docs/COMMANDS.md#artifact) |
| `artifact` | Assert artifacts exist (with size budgets) and generate size/SHA256 manifests | [↗](docs/COMMANDS.md#artifact) |
| `archive` | Pack, list, and unpack zip/tar/tar.gz/tar.xz/tar.zst archives | [↗](docs/COMMANDS.md#archive) |
| `git` | CI-friendly git metadata: ref, SHA, tags, dirty state | [↗](docs/COMMANDS.md#git) |
| `changelog` | Generate release notes from git commit ranges | [↗](docs/COMMANDS.md#changelog) |
| `config` | Resolve env-specific config maps; map branches to environments | [↗](docs/COMMANDS.md#config) |
| `parse` | Pull fenced code blocks / YAML / frontmatter out of issue bodies, PR comments, markdown | [↗](docs/COMMANDS.md#parse) |
| `comment` | Render, inspect, select, and amend hidden-anchor PR comments | [↗](docs/COMMANDS.md#comment) |
| `json` / `yaml` | Get / set / del / deep-merge / convert / pretty / table on JSON, YAML, TOML, CSV | [↗](docs/COMMANDS.md#json) |
| `render` | Render Go templates with a sprig-like FuncMap and stacked `--values` files | [↗](docs/COMMANDS.md#render) |
| `json` / `yaml` | Get / set / del / semantic diff / deep-merge / convert / pretty / table on JSON, YAML, TOML, CSV — incl. multi-doc YAML addressing (`--doc`) | [↗](docs/COMMANDS.md#json) |
| `render` | Render Go templates with a sprig-like FuncMap and stacked `--values` files; `--envsubst` mode | [↗](docs/COMMANDS.md#render) |
| `exec` | Unified retry + mask + tee + timeout command runner | [↗](docs/COMMANDS.md#exec) |
| `http` | Curl-like HTTP requests, JSON extraction, uploads, and chained flows | [↗](docs/COMMANDS.md#http) |
| `http` | Curl-like HTTP requests, JSON extraction, uploads, chained flows, and Link-header pagination | [↗](docs/COMMANDS.md#http) |
| `url parse` | Split a URL into `SCHEME / HOST / PORT / USER / PASSWORD / PATH / QUERY` env vars | [↗](docs/COMMANDS.md#url) |
| `image parse` | Split a container image ref into registry / repository / tag / digest | [↗](docs/COMMANDS.md#image) |
| `time` | RFC3339 / unix / tag / compact / iso timestamps; format conversion; arithmetic | [↗](docs/COMMANDS.md#time) |
| `port free` · `uuid` · `random` | Tiny generators for ephemeral resource names and ports | [↗](docs/COMMANDS.md#misc) |
| `doctor` | Diagnose CI platform, env-var contract, tool availability | [↗](docs/COMMANDS.md#doctor) |
| `report` | Parse JUnit XML / lcov / cobertura into summaries, outputs, and threshold gates | [↗](docs/COMMANDS.md#report) |
| `annotate` | Emit inline PR annotations (GitHub) or plain diagnostics; convert linter JSON | [↗](docs/COMMANDS.md#annotate) |
| `lock` | File-based mutex: acquire / release / run with timeout and stale takeover | [↗](docs/COMMANDS.md#lock) |

Full flag reference → **[docs/COMMANDS.md](docs/COMMANDS.md)**

Expand Down
114 changes: 114 additions & 0 deletions actions/annotate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package actions

import (
"fmt"

"github.com/AxeForging/pipekit/services"

"github.com/urfave/cli"
)

// AnnotateCommand returns the CI annotation command group.
func AnnotateCommand() cli.Command {
levelCmd := func(level, usage string) cli.Command {
return cli.Command{
Name: level,
Usage: usage,
ArgsUsage: "MESSAGE",
Flags: annotateFlags(),
Action: func(c *cli.Context) error {
msg, err := firstArgOrErr(c, "MESSAGE")
if err != nil {
return err
}
a := services.Annotation{
Level: level,
File: c.String("file"),
Line: c.Int("line"),
EndLine: c.Int("end-line"),
Col: c.Int("col"),
Title: c.String("title"),
Message: msg,
}
fmt.Println(services.FormatAnnotation(a, annotatePlatform(c)))
return nil
},
}
}

return cli.Command{
Name: "annotate",
Usage: "emit CI annotations (inline PR comments on GitHub, plain lines elsewhere)",
Subcommands: []cli.Command{
levelCmd("error", "emit an error annotation"),
levelCmd("warning", "emit a warning annotation"),
levelCmd("notice", "emit a notice annotation"),
{
Name: "from-json",
Usage: "convert a JSON array of linter findings into annotations",
ArgsUsage: "[FILE]",
Flags: []cli.Flag{
cli.StringFlag{Name: "file-key", Value: "file", Usage: "field holding the file path"},
cli.StringFlag{Name: "line-key", Value: "line", Usage: "field holding the line number"},
cli.StringFlag{Name: "col-key", Value: "col", Usage: "field holding the column"},
cli.StringFlag{Name: "message-key", Value: "message", Usage: "field holding the message"},
cli.StringFlag{Name: "severity-key", Value: "severity", Usage: "field holding the severity"},
cli.StringFlag{Name: "title-key", Value: "title", Usage: "field holding the title"},
cli.StringFlag{Name: "platform", Usage: "force output platform: github-actions, plain (default: auto-detect)"},
cli.BoolFlag{Name: "fail-on-errors", Usage: "exit 1 when any finding normalizes to error level"},
},
Action: func(c *cli.Context) error {
data, err := readReportInput(c)
if err != nil {
return err
}
keys := services.AnnotationKeys{
File: c.String("file-key"),
Line: c.String("line-key"),
Col: c.String("col-key"),
Message: c.String("message-key"),
Severity: c.String("severity-key"),
Title: c.String("title-key"),
}
annotations, err := services.ParseAnnotationsJSON(data, keys)
if err != nil {
return err
}
platform := annotatePlatform(c)
errors := 0
for _, a := range annotations {
if services.NormalizeAnnotationLevel(a.Level) == "error" {
errors++
}
fmt.Println(services.FormatAnnotation(a, platform))
}
if c.Bool("fail-on-errors") && errors > 0 {
return cli.NewExitError(fmt.Sprintf("%d error finding(s)", errors), 1)
}
return nil
},
},
},
}
}

func annotateFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{Name: "file, f", Usage: "source file the annotation points at"},
cli.IntFlag{Name: "line, l", Usage: "line number"},
cli.IntFlag{Name: "end-line", Usage: "end line for a range"},
cli.IntFlag{Name: "col", Usage: "column number"},
cli.StringFlag{Name: "title, t", Usage: "annotation title"},
cli.StringFlag{Name: "platform", Usage: "force output platform: github-actions, plain (default: auto-detect)"},
}
}

func annotatePlatform(c *cli.Context) string {
if p := c.String("platform"); p != "" {
if p == "github" {
return "github-actions"
}
return p
}
return services.DetectCI().Name
}
27 changes: 24 additions & 3 deletions actions/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,43 @@ func ArtifactCommand() cli.Command {
}
out += "\n"
if path := c.String("output"); path != "" {
return os.WriteFile(path, []byte(out), 0644)
return os.WriteFile(path, []byte(out), 0o644)
}
fmt.Print(out)
return nil
},
},
{
Name: "assert",
Usage: "fail unless each artifact path or glob resolves",
Usage: "fail unless each artifact path or glob resolves (and fits the size budget)",
ArgsUsage: "PATH_OR_GLOB...",
Flags: []cli.Flag{
cli.StringFlag{Name: "max-size", Usage: "fail when any matched file exceeds this size (e.g. 5MB, 100KiB)"},
cli.StringFlag{Name: "min-size", Usage: "fail when any matched file is smaller than this size"},
},
Action: func(c *cli.Context) error {
patterns, err := argsOrErr(c, "artifact path or glob")
if err != nil {
return err
}
return services.AssertArtifacts(patterns)
if err := services.AssertArtifacts(patterns); err != nil {
return err
}
if c.String("max-size") == "" && c.String("min-size") == "" {
return nil
}
var min, max int64
if v := c.String("max-size"); v != "" {
if max, err = services.ParseSize(v); err != nil {
return cli.NewExitError(err.Error(), 1)
}
}
if v := c.String("min-size"); v != "" {
if min, err = services.ParseSize(v); err != nil {
return cli.NewExitError(err.Error(), 1)
}
}
return services.AssertArtifactSizes(patterns, min, max)
},
},
},
Expand Down
18 changes: 18 additions & 0 deletions actions/assert.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package actions

import (
"fmt"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -177,6 +178,23 @@ func AssertCommand() cli.Command {
return nil
},
},
{
Name: "versions-match",
Usage: "assert all files declare the same version (monorepo release guard)",
ArgsUsage: "FILE FILE...",
Action: func(c *cli.Context) error {
files := c.Args()
if len(files) < 2 {
return cli.NewExitError("at least two files required", 1)
}
version, err := services.AssertVersionsMatch(files)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
fmt.Println(version)
return nil
},
},
{
Name: "path",
Usage: "assert one or more paths exist (file or directory)",
Expand Down
43 changes: 33 additions & 10 deletions actions/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ func httpMethodCommand(method string) cli.Command {
cli.BoolFlag{Name: "backoff", Usage: "exponential backoff between retries"},
cli.BoolFlag{Name: "insecure, k", Usage: "skip TLS certificate verification"},
cli.BoolFlag{Name: "verbose, v", Usage: "print response status and headers to stderr"},
cli.BoolFlag{Name: "paginate", Usage: "follow Link rel=\"next\" headers and merge all pages (GET only)"},
cli.StringFlag{Name: "items", Usage: "jq-style path to the items array inside each page (default: body is the array)"},
cli.IntFlag{Name: "max-pages", Value: 50, Usage: "pagination safety cap"},
},
Action: func(c *cli.Context) error {
urlStr, err := firstArgOrErr(c, "URL")
Expand Down Expand Up @@ -98,7 +101,7 @@ func httpMethodCommand(method string) cli.Command {
return cli.NewExitError(err.Error(), 1)
}

res, err := services.ExecuteHTTPRequest(services.HTTPRequestOptions{
opts := services.HTTPRequestOptions{
Method: method,
URL: urlStr,
Headers: headers,
Expand All @@ -109,24 +112,44 @@ func httpMethodCommand(method string) cli.Command {
RetryDelay: retryDelay,
Backoff: c.Bool("backoff"),
InsecureTLS: c.Bool("insecure"),
})
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
if c.Bool("verbose") {
printHTTPVerbose(res)
}

out := res.Body
var out []byte
if c.Bool("paginate") {
if method != "GET" {
return cli.NewExitError("--paginate only works with GET", 1)
}
items, pages, err := services.ExecuteHTTPPaginated(opts, c.String("items"), c.Int("max-pages"))
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
if c.Bool("verbose") {
fmt.Fprintf(os.Stderr, "fetched %d item(s) across %d page(s)\n", len(items), pages)
}
out, err = json.Marshal(items)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
out = append(out, '\n')
} else {
res, err := services.ExecuteHTTPRequest(opts)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
if c.Bool("verbose") {
printHTTPVerbose(res)
}
out = res.Body
}
if path := c.String("jq"); path != "" {
val, err := services.ExtractHTTPJSON(res.Body, path)
val, err := services.ExtractHTTPJSON(out, path)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
out = []byte(formatHTTPResult(val, c.Bool("raw")))
}
if output := c.String("output"); output != "" {
return os.WriteFile(output, out, 0644)
return os.WriteFile(output, out, 0o644)
}
if outputKey := c.String("to-github-output"); outputKey != "" {
return services.WriteToGitHubOutputValue(outputKey, strings.TrimRight(string(out), "\n"))
Expand Down
Loading
Loading