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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

All notable changes to TagLock will be documented here.

## Unreleased

No unreleased changes yet.

## 0.4.0 - 2026-08-14

### Added

- `taglock check --format github` emits GitHub Actions workflow commands (`::error`, `::warning`, `::notice`) so pull-request files are annotated without a SARIF upload.
- `output.GitHub` for embedders that want the same annotation stream.

### Compatibility

- Existing analyzer, CLI, snapshot, schema, verification, and evolution APIs are unchanged.
- `text`, `json`, and `sarif` formats remain the default and previous options.
- The release is additive and requires no configuration migration.

## 0.3.0 - 2026-08-11

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ go install github.com/theworker02/taglock/cmd/taglock@latest
taglock check ./...
taglock check --format json --fail-on error ./...
taglock check --format sarif ./... > taglock.sarif
taglock check --format github ./...
taglock check --json-semantics v2 ./...
```

Expand Down
26 changes: 26 additions & 0 deletions docs/releases/v0.4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# TagLock v0.4.0 — GitHub Actions annotations

TagLock v0.4.0 adds a GitHub Actions workflow-command report format so `taglock check` can annotate pull-request files without uploading SARIF.

## Added

- `taglock check --format github` emits `::error`, `::warning`, and `::notice` commands with file, line, column, and rule title.
- `output.GitHub` for library embedders.

## Compatibility

- Existing analyzer, CLI, snapshot, schema, verification, and evolution APIs are unchanged.
- `text`, `json`, and `sarif` remain supported. The default check format is still `text`.
- Go 1.24 remains the minimum supported toolchain.

## Upgrade

```bash
go get github.com/theworker02/taglock@v0.4.0
```

## Example

```bash
taglock check --format github --fail-on error ./...
```
6 changes: 4 additions & 2 deletions internal/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func Run(arguments []string, stdout, stderr io.Writer) int {

func runCheck(arguments []string, stdout, stderr io.Writer) int {
set := newFlagSet("check", stderr)
format := set.String("format", "text", "output format: text, json, or sarif")
format := set.String("format", "text", "output format: text, json, sarif, or github")
failOn := set.String("fail-on", "warning", "minimum severity that fails")
configPath := set.String("config", "", "configuration path")
baselinePath := set.String("baseline", "", "baseline file")
Expand All @@ -115,7 +115,7 @@ func runCheck(arguments []string, stdout, stderr io.Writer) int {
fmt.Fprintln(stderr, "taglock:", err)
return ExitUsage
}
if *format != "text" && *format != "json" && *format != "sarif" {
if *format != "text" && *format != "json" && *format != "sarif" && *format != "github" {
fmt.Fprintf(stderr, "taglock: invalid format %q\n", *format)
return ExitUsage
}
Expand Down Expand Up @@ -170,6 +170,8 @@ func runCheck(arguments []string, stdout, stderr io.Writer) int {
err = output.JSON(stdout, result.FileSet, diagnostics)
case "sarif":
err = output.SARIF(stdout, result.FileSet, diagnostics)
case "github":
err = output.GitHub(stdout, result.FileSet, diagnostics)
}
if err != nil {
fmt.Fprintln(stderr, "taglock: write output:", err)
Expand Down
11 changes: 11 additions & 0 deletions internal/cli/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ func TestRulesAndExplainShareCatalog(t *testing.T) {
}
}

func TestGitHubCheckFormat(t *testing.T) {
var out, errOut bytes.Buffer
code := cli.Run([]string{"check", "--format", "github", "./testdata/violation"}, &out, &errOut)
if code != cli.ExitViolations {
t.Fatalf("check github code=%d out=%s err=%s", code, out.String(), errOut.String())
}
if !strings.Contains(out.String(), "::error ") || !strings.Contains(out.String(), "title=") {
t.Fatalf("expected GitHub workflow commands, got %s", out.String())
}
}

func TestVersionCommand(t *testing.T) {
var out, errOut bytes.Buffer
if code := cli.Run([]string{"version"}, &out, &errOut); code != cli.ExitOK {
Expand Down
42 changes: 42 additions & 0 deletions output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/theworker02/taglock/baseline"
"github.com/theworker02/taglock/rule"
"github.com/theworker02/taglock/rules"
)

Expand Down Expand Up @@ -85,6 +86,47 @@ func JSON(writer io.Writer, fileSet *token.FileSet, diagnostics []rules.Diagnost
return encoder.Encode(JSONDocument{SchemaVersion: JSONSchemaVersion, Findings: Flatten(fileSet, diagnostics)})
}

// GitHub writes GitHub Actions workflow commands so `check --format github`
// annotates pull-request files without a SARIF upload step.
func GitHub(writer io.Writer, fileSet *token.FileSet, diagnostics []rules.Diagnostic) error {
for _, diagnostic := range diagnostics {
position := fileSet.Position(diagnostic.Pos)
command := githubCommand(diagnostic.Severity)
title := githubEscapeProperty(diagnostic.Rule.ID)
file := githubEscapeProperty(position.Filename)
message := githubEscapeMessage(diagnostic.Rule.ID + " " + diagnostic.Message)
if _, err := fmt.Fprintf(writer, "::%s file=%s,line=%d,col=%d,title=%s::%s\n", command, file, position.Line, position.Column, title, message); err != nil {
return err
}
}
return nil
}

func githubCommand(severity rule.Severity) string {
switch severity {
case rule.SeverityError:
return "error"
case rule.SeverityWarning:
return "warning"
default:
return "notice"
}
}

func githubEscapeMessage(value string) string {
value = strings.ReplaceAll(value, "%", "%25")
value = strings.ReplaceAll(value, "\r", "%0D")
value = strings.ReplaceAll(value, "\n", "%0A")
return value
}

func githubEscapeProperty(value string) string {
value = githubEscapeMessage(value)
value = strings.ReplaceAll(value, ":", "%3A")
value = strings.ReplaceAll(value, ",", "%2C")
return value
}

type sarifLog struct {
Version string `json:"version"`
Schema string `json:"$schema"`
Expand Down
14 changes: 14 additions & 0 deletions output/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,17 @@ func TestSARIFContainsRuleAndLocation(t *testing.T) {
}
}
}

func TestGitHubWorkflowCommands(t *testing.T) {
set, diagnostics := testDiagnostic()
var buffer bytes.Buffer
if err := output.GitHub(&buffer, set, diagnostics); err != nil {
t.Fatal(err)
}
value := buffer.String()
for _, expected := range []string{"::error file=model.go,line=2,col=", "title=TAG104::", "TAG104 duplicate json name"} {
if !strings.Contains(value, expected) {
t.Fatalf("missing %s in %s", expected, value)
}
}
}