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
105 changes: 105 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: CI

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Unit tests
run: go test ./...

- name: Build pipekit
run: make build

- name: Integration tests
run: go test ./integration/... -v

- name: Export env from JSON
run: |
./dist/pipekit env from-json --flatten --uppercase-keys --to-github <<'JSON'
{
"name": "pipekit",
"ci": {
"platform": "github-actions",
"purpose": "dogfood"
}
}
JSON

- name: Assert exported env in later step
run: |
./dist/pipekit assert env-exists NAME CI_PLATFORM CI_PURPOSE
test "$NAME" = "pipekit"
test "$CI_PLATFORM" = "github-actions"
test "$CI_PURPOSE" = "dogfood"

- name: Export outputs from JSON
id: json_outputs
run: |
./dist/pipekit env from-json --uppercase-keys --to-github-output <<'JSON'
{
"artifact": "pipekit",
"channel": "ci"
}
JSON

- name: Assert exported outputs in later step
env:
ARTIFACT: ${{ steps.json_outputs.outputs.ARTIFACT }}
CHANNEL: ${{ steps.json_outputs.outputs.CHANNEL }}
run: |
./dist/pipekit assert env-exists ARTIFACT CHANNEL
test "$ARTIFACT" = "pipekit"
test "$CHANNEL" = "ci"

- name: Export cache key
id: cache_key
run: ./dist/pipekit cache-key from-files go.sum --prefix "go-" --to-github-output cache_key

- name: Assert cache key output in later step
env:
CACHE_KEY: ${{ steps.cache_key.outputs.cache_key }}
run: |
./dist/pipekit assert env-exists CACHE_KEY
case "$CACHE_KEY" in
go-*) ;;
*) echo "cache key missing go- prefix: $CACHE_KEY"; exit 1 ;;
esac

- name: Dogfood JSON, mask, exec, and summary
run: |
printf '{"module":"github.com/AxeForging/pipekit","kind":"ci"}\n' > pipekit-ci.json
test "$(./dist/pipekit json get pipekit-ci.json --path '.module' --raw)" = "github.com/AxeForging/pipekit"
./dist/pipekit assert json-path --file pipekit-ci.json --path '.kind' --expected "ci"
./dist/pipekit git sha --short --to-github-output git_sha
./dist/pipekit git ref --slug --to-github-output ref_slug
./dist/pipekit checksum files dist/pipekit --output pipekit-checksums.txt
./dist/pipekit checksum verify pipekit-checksums.txt
./dist/pipekit artifact assert dist/pipekit pipekit-checksums.txt
./dist/pipekit artifact manifest dist/pipekit pipekit-checksums.txt --pretty --output pipekit-artifacts.json
./dist/pipekit changelog generate --from origin/main --conventional --output pipekit-changelog.md
./dist/pipekit mask github "secret-ci-value"
./dist/pipekit exec --attempts 2 --delay 1s --mask "secret-[a-z-]+" --tee pipekit-ci.log -- sh -c 'echo token=secret-ci-value'
./dist/pipekit summary badge --label "pipekit" --status success --to-github-summary
./dist/pipekit summary section --title "pipekit artifacts" --to-github-summary < pipekit-artifacts.json
./dist/pipekit summary section --title "pipekit CI log" --to-github-summary < pipekit-ci.log
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
go-version: '1.24'

- name: Run tests
run: go test ./...
run: make test-integration

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build clean test lint tidy version release-check
.PHONY: all build clean test test-integration lint tidy version release-check

GOOS_ARCH := linux/amd64 linux/arm64 linux/386 linux/arm darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 windows/386
DIST_DIR := dist
Expand Down Expand Up @@ -45,6 +45,11 @@ test:
go test ./... -v
@echo "All tests passed."

test-integration: build
@echo "Running integration tests against dist/pipekit..."
go test ./integration/... -v
@echo "Integration tests passed."

lint:
@echo "Running linter..."
golangci-lint run --timeout=5m
Expand All @@ -71,7 +76,7 @@ tag:
git tag -a $(VERSION) -m "Release $(VERSION)"
@echo "Tag created. Push with: git push origin $(VERSION)"

release-check: build
release-check: test-integration
@echo "Running tests..."
go test ./...
@echo "All tests passed. Ready for release $(VERSION)"
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ More end-to-end recipes → **[docs/EXAMPLES.md](docs/EXAMPLES.md)**
| `version` | Get / bump / compare versions across `package.json`, `Cargo.toml`, `Chart.yaml`, etc. | [↗](docs/COMMANDS.md#version) |
| `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) |
| `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) |
| `json` / `yaml` | Get / set / del / deep-merge / convert / pretty / table on JSON, YAML, TOML, CSV | [↗](docs/COMMANDS.md#json) |
Expand Down
61 changes: 61 additions & 0 deletions actions/artifact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package actions

import (
"fmt"
"os"

"github.com/AxeForging/pipekit/services"

"github.com/urfave/cli"
)

// ArtifactCommand returns CI artifact helpers.
func ArtifactCommand() cli.Command {
return cli.Command{
Name: "artifact",
Usage: "inspect and validate release artifacts",
Subcommands: []cli.Command{
{
Name: "manifest",
Usage: "generate a JSON manifest for files or globs",
ArgsUsage: "PATH_OR_GLOB...",
Flags: []cli.Flag{
cli.BoolFlag{Name: "pretty", Usage: "pretty-print JSON"},
cli.StringFlag{Name: "output, o", Usage: "write manifest to this file"},
},
Action: func(c *cli.Context) error {
patterns, err := argsOrErr(c, "artifact path or glob")
if err != nil {
return err
}
entries, err := services.ArtifactManifest(patterns)
if err != nil {
return err
}
out, err := services.FormatArtifactManifestJSON(entries, c.Bool("pretty"))
if err != nil {
return err
}
out += "\n"
if path := c.String("output"); path != "" {
return os.WriteFile(path, []byte(out), 0644)
}
fmt.Print(out)
return nil
},
},
{
Name: "assert",
Usage: "fail unless each artifact path or glob resolves",
ArgsUsage: "PATH_OR_GLOB...",
Action: func(c *cli.Context) error {
patterns, err := argsOrErr(c, "artifact path or glob")
if err != nil {
return err
}
return services.AssertArtifacts(patterns)
},
},
},
}
}
96 changes: 96 additions & 0 deletions actions/changelog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package actions

import (
"encoding/json"
"fmt"
"os"

"github.com/AxeForging/pipekit/services"

"github.com/urfave/cli"
)

// ChangelogCommand returns release-note generation helpers.
func ChangelogCommand() cli.Command {
return cli.Command{
Name: "changelog",
Usage: "generate release notes from git commits",
Subcommands: []cli.Command{
{
Name: "generate",
Usage: "generate changelog markdown for a git range",
Flags: []cli.Flag{
cli.StringFlag{Name: "from", Usage: "start ref (exclusive)"},
cli.StringFlag{Name: "to", Value: "HEAD", Usage: "end ref (inclusive)"},
cli.BoolFlag{Name: "conventional", Usage: "group conventional commits"},
cli.StringFlag{Name: "format, f", Value: "markdown", Usage: "markdown or json"},
cli.StringFlag{Name: "output, o", Usage: "write output to this file"},
outputKeyFlag(),
},
Action: func(c *cli.Context) error {
markdown, entries, err := services.GenerateChangelog(services.ChangelogOptions{
From: c.String("from"),
To: c.String("to"),
Conventional: c.Bool("conventional"),
})
if err != nil {
return err
}

out := markdown
switch c.String("format") {
case "markdown", "":
case "json":
data, err := json.MarshalIndent(entries, "", " ")
if err != nil {
return err
}
out = string(data) + "\n"
default:
return cli.NewExitError("unknown format: "+c.String("format"), 1)
}

if outputKey := c.String("to-github-output"); outputKey != "" {
return services.WriteToGitHubOutputValue(outputKey, out)
}
if path := c.String("output"); path != "" {
return os.WriteFile(path, []byte(out), 0644)
}
fmt.Print(out)
return nil
},
},
{
Name: "since-tag",
Usage: "generate changelog markdown since the latest reachable tag",
Flags: []cli.Flag{
cli.BoolFlag{Name: "conventional", Usage: "group conventional commits"},
cli.StringFlag{Name: "output, o", Usage: "write output to this file"},
outputKeyFlag(),
},
Action: func(c *cli.Context) error {
tag, err := services.GitPreviousTag()
if err != nil {
return err
}
markdown, _, err := services.GenerateChangelog(services.ChangelogOptions{
From: tag,
To: "HEAD",
Conventional: c.Bool("conventional"),
})
if err != nil {
return err
}
if outputKey := c.String("to-github-output"); outputKey != "" {
return services.WriteToGitHubOutputValue(outputKey, markdown)
}
if path := c.String("output"); path != "" {
return os.WriteFile(path, []byte(markdown), 0644)
}
fmt.Print(markdown)
return nil
},
},
},
}
}
74 changes: 74 additions & 0 deletions actions/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package actions

import (
"encoding/json"
"fmt"
"os"

"github.com/AxeForging/pipekit/services"

"github.com/urfave/cli"
)

// ChecksumCommand returns checksum helpers for release artifacts.
func ChecksumCommand() cli.Command {
return cli.Command{
Name: "checksum",
Usage: "generate and verify file checksums",
Subcommands: []cli.Command{
{
Name: "files",
Usage: "hash one or more files independently",
ArgsUsage: "FILE...",
Flags: []cli.Flag{
cli.StringFlag{Name: "algorithm, a", Value: "sha256", Usage: "sha256, sha1, or md5"},
cli.StringFlag{Name: "format, f", Value: "text", Usage: "text or json"},
cli.StringFlag{Name: "output, o", Usage: "write output to this file"},
},
Action: func(c *cli.Context) error {
files, err := argsOrErr(c, "file")
if err != nil {
return err
}
sums, err := services.ChecksumFiles(files, c.String("algorithm"))
if err != nil {
return err
}
var out string
switch c.String("format") {
case "json":
data, err := json.MarshalIndent(sums, "", " ")
if err != nil {
return err
}
out = string(data) + "\n"
case "text", "":
out = services.FormatChecksums(sums)
default:
return cli.NewExitError("unknown format: "+c.String("format"), 1)
}
if path := c.String("output"); path != "" {
return os.WriteFile(path, []byte(out), 0644)
}
fmt.Print(out)
return nil
},
},
{
Name: "verify",
Usage: "verify a checksum file",
ArgsUsage: "CHECKSUM_FILE",
Flags: []cli.Flag{
cli.StringFlag{Name: "algorithm, a", Value: "sha256", Usage: "sha256, sha1, or md5"},
},
Action: func(c *cli.Context) error {
path, err := firstArgOrErr(c, "CHECKSUM_FILE")
if err != nil {
return err
}
return services.VerifyChecksums(path, c.String("algorithm"))
},
},
},
}
}
Loading
Loading