Enforce redaction for resolved pipeline summaries
This commit is contained in:
@@ -156,7 +156,7 @@ exclusivity, or failure reporting.
|
|||||||
|
|
||||||
## Stage 1: Enforce redaction for resolved-pipeline summaries
|
## Stage 1: Enforce redaction for resolved-pipeline summaries
|
||||||
|
|
||||||
**Status:** Not started
|
**Status:** Complete
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
if err := writeSummary(summary, func() error { return summary.WriteRedactedEffectiveConfig(effective) }); err != nil {
|
if err := writeSummary(summary, func() error { return summary.WriteRedactedEffectiveConfig(effective) }); err != nil {
|
||||||
return failPipelineCommand(stderr, summary, debugPath, fmt.Errorf("write debug effective config: %w", err), false)
|
return failPipelineCommand(stderr, summary, debugPath, fmt.Errorf("write debug effective config: %w", err), false)
|
||||||
}
|
}
|
||||||
if err := writeSummary(summary, func() error { return summary.WriteResolvedPipeline(effective.ResolvedPipeline) }); err != nil {
|
if err := writeSummary(summary, func() error { return summary.WriteResolvedPipeline(effective) }); err != nil {
|
||||||
return failPipelineCommand(stderr, summary, debugPath, fmt.Errorf("write debug resolved pipeline: %w", err), false)
|
return failPipelineCommand(stderr, summary, debugPath, fmt.Errorf("write debug resolved pipeline: %w", err), false)
|
||||||
}
|
}
|
||||||
if err := writeSummary(summary, func() error {
|
if err := writeSummary(summary, func() error {
|
||||||
|
|||||||
@@ -248,6 +248,49 @@ func TestRunDebugArtifactsRedactSecretsButRetainApplicationData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunRedactsSensitiveModuleOptionsFromConfigAndPipelineSummaries(t *testing.T) {
|
||||||
|
roots := newStateTestRoots(t)
|
||||||
|
data, err := os.ReadFile(roots.config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
configText := strings.Replace(string(data), " input: test/input\n", ` input:
|
||||||
|
module: test/input
|
||||||
|
options:
|
||||||
|
api_key: CONFIG_SUMMARY_SECRET_SENTINEL
|
||||||
|
safe: SAFE_OPTION_SENTINEL
|
||||||
|
nested:
|
||||||
|
- - password: PIPELINE_SUMMARY_SECRET_SENTINEL
|
||||||
|
neighbor: SAFE_NESTED_OPTION_SENTINEL
|
||||||
|
`, 1)
|
||||||
|
if err := os.WriteFile(roots.config, []byte(configText), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := runStateTest(t, roots, newStateTestHarness().options(), true, false, "bypass")
|
||||||
|
if result.code != 0 {
|
||||||
|
t.Fatalf("code=%d stderr=%q", result.code, result.stderr)
|
||||||
|
}
|
||||||
|
summaryRoot := filepath.Join(onlyChildDir(t, roots.debug), "summary")
|
||||||
|
for _, name := range []string{"effective-config.json", "resolved-pipeline.json"} {
|
||||||
|
contents, err := os.ReadFile(filepath.Join(summaryRoot, name))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
text := string(contents)
|
||||||
|
for _, secret := range []string{"CONFIG_SUMMARY_SECRET_SENTINEL", "PIPELINE_SUMMARY_SECRET_SENTINEL"} {
|
||||||
|
if strings.Contains(text, secret) {
|
||||||
|
t.Fatalf("%s contains %q: %s", name, secret, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, retained := range []string{"[REDACTED]", "SAFE_OPTION_SENTINEL", "SAFE_NESTED_OPTION_SENTINEL"} {
|
||||||
|
if !strings.Contains(text, retained) {
|
||||||
|
t.Fatalf("%s does not contain %q: %s", name, retained, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type stateTestRoots struct{ config, input, output, plans, checkpoints, debug string }
|
type stateTestRoots struct{ config, input, output, plans, checkpoints, debug string }
|
||||||
|
|
||||||
func newStateTestRoots(t *testing.T) stateTestRoots {
|
func newStateTestRoots(t *testing.T) stateTestRoots {
|
||||||
@@ -422,7 +465,7 @@ func (h *stateTestHarness) options() Options {
|
|||||||
if err := pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, stateTestCodec{}); err != nil {
|
if err := pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, stateTestCodec{}); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := registries.Inputs.RegisterWithSpec(pipeline.ModuleSpec{Key: "test/input", Stage: pipeline.StageInput, Provides: []string{"source"}}, func() (contracts.InputAdapter, error) { return stateTestInput{}, nil }); err != nil {
|
if err := registries.Inputs.RegisterBuilderWithSpec(pipeline.ModuleSpec{Key: "test/input", Stage: pipeline.StageInput, Provides: []string{"source"}}, func(map[string]any) error { return nil }, func(pipeline.BuildRequest) (contracts.InputAdapter, error) { return stateTestInput{}, nil }); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := registries.Chunkers.RegisterWithSpec(pipeline.ModuleSpec{Key: "test/chunk", Stage: pipeline.StageChunk, Requires: []string{"source"}, Provides: []string{"chunks"}}, func() (contracts.Chunker, error) { return stateTestChunker{h}, nil }); err != nil {
|
if err := registries.Chunkers.RegisterWithSpec(pipeline.ModuleSpec{Key: "test/chunk", Stage: pipeline.StageChunk, Requires: []string{"source"}, Provides: []string{"chunks"}}, func() (contracts.Chunker, error) { return stateTestChunker{h}, nil }); err != nil {
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ func (e EffectiveConfig) RedactedSummaryPayload() any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e EffectiveConfig) RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline {
|
||||||
|
return cloneResolvedPipeline(e.ResolvedPipeline)
|
||||||
|
}
|
||||||
|
|
||||||
func cloneResolvedPipeline(in pipeline.ResolvedPipeline) pipeline.ResolvedPipeline {
|
func cloneResolvedPipeline(in pipeline.ResolvedPipeline) pipeline.ResolvedPipeline {
|
||||||
out := in
|
out := in
|
||||||
out.Input = redactBinding(cloneModuleBinding(in.Input))
|
out.Input = redactBinding(cloneModuleBinding(in.Input))
|
||||||
@@ -116,26 +120,26 @@ func redactOptions(values map[string]any) map[string]any {
|
|||||||
out[key] = "[REDACTED]"
|
out[key] = "[REDACTED]"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch typed := value.(type) {
|
out[key] = redactOptionValue(value)
|
||||||
case map[string]any:
|
|
||||||
out[key] = redactOptions(typed)
|
|
||||||
case []any:
|
|
||||||
items := make([]any, len(typed))
|
|
||||||
for i, item := range typed {
|
|
||||||
if nested, ok := item.(map[string]any); ok {
|
|
||||||
items[i] = redactOptions(nested)
|
|
||||||
} else {
|
|
||||||
items[i] = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out[key] = items
|
|
||||||
default:
|
|
||||||
out[key] = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func redactOptionValue(value any) any {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
return redactOptions(typed)
|
||||||
|
case []any:
|
||||||
|
items := make([]any, len(typed))
|
||||||
|
for i, item := range typed {
|
||||||
|
items[i] = redactOptionValue(item)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
default:
|
||||||
|
return typed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sensitiveConfigKey(key string) bool {
|
func sensitiveConfigKey(key string) bool {
|
||||||
key = strings.ToLower(key)
|
key = strings.ToLower(key)
|
||||||
return strings.Contains(key, "api_key") || strings.Contains(key, "apikey") || strings.Contains(key, "authorization") || strings.Contains(key, "bearer") || strings.Contains(key, "password") || strings.Contains(key, "secret") || strings.Contains(key, "token")
|
return strings.Contains(key, "api_key") || strings.Contains(key, "apikey") || strings.Contains(key, "authorization") || strings.Contains(key, "bearer") || strings.Contains(key, "password") || strings.Contains(key, "secret") || strings.Contains(key, "token")
|
||||||
|
|||||||
169
internal/core/config/redaction_test.go
Normal file
169
internal/core/config/redaction_test.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRedactedResolvedPipelinePayloadRedactsEveryBinding(t *testing.T) {
|
||||||
|
bindings := map[string]pipeline.ModuleBinding{}
|
||||||
|
for _, name := range []string{
|
||||||
|
"input", "chunk", "output", "extract", "merge", "normalize",
|
||||||
|
"resolved-validator", "lane-validator",
|
||||||
|
} {
|
||||||
|
bindings[name] = redactionTestBinding(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved := pipeline.ResolvedPipeline{
|
||||||
|
ID: "redaction-test",
|
||||||
|
Digest: "sha256:safe-digest",
|
||||||
|
Input: bindings["input"],
|
||||||
|
Chunk: bindings["chunk"],
|
||||||
|
ChunkReferences: redactionTestReferenceTarget(pipeline.StageChunk, "", "chunk-reference-content"),
|
||||||
|
ArtifactLanes: []pipeline.ResolvedArtifactLane{{
|
||||||
|
ID: "safe-lane",
|
||||||
|
ArtifactKind: "safe/artifact",
|
||||||
|
Extract: bindings["extract"],
|
||||||
|
Merge: bindings["merge"],
|
||||||
|
Normalize: bindings["normalize"],
|
||||||
|
Validators: []pipeline.ModuleBinding{bindings["lane-validator"]},
|
||||||
|
ExtractReferences: redactionTestReferenceTarget(pipeline.StageExtract, "safe-lane", "extract-reference-content"),
|
||||||
|
MergeReferences: redactionTestReferenceTarget(pipeline.StageMerge, "safe-lane", "merge-reference-content"),
|
||||||
|
NormalizeReferences: redactionTestReferenceTarget(pipeline.StageNormalize, "safe-lane", "normalize-reference-content"),
|
||||||
|
}},
|
||||||
|
ValidatorChains: []pipeline.ResolvedValidatorChain{{
|
||||||
|
Stage: pipeline.StageExtract,
|
||||||
|
LaneID: "safe-lane",
|
||||||
|
ModuleKey: "safe-extract-owner",
|
||||||
|
Validators: []pipeline.ResolvedValidator{{
|
||||||
|
Binding: bindings["resolved-validator"],
|
||||||
|
ExecutionClass: contracts.ExecutionClassDeterministic,
|
||||||
|
Target: pipeline.ValidatorTargetTyped,
|
||||||
|
ArtifactKind: "safe/artifact",
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
Output: bindings["output"],
|
||||||
|
}
|
||||||
|
effective := EffectiveConfig{
|
||||||
|
Config: Config{Pipelines: map[string]pipeline.PipelineProfile{
|
||||||
|
"redaction-test": {Input: bindings["input"]},
|
||||||
|
}},
|
||||||
|
PipelineID: "redaction-test",
|
||||||
|
ResolvedPipeline: resolved,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := effective.RedactedResolvedPipelinePayload()
|
||||||
|
encoded, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
text := string(encoded)
|
||||||
|
for name := range bindings {
|
||||||
|
for _, forbidden := range []string{name + "-secret", name + "-nested-secret"} {
|
||||||
|
if strings.Contains(text, forbidden) {
|
||||||
|
t.Fatalf("resolved pipeline summary contains %q: %s", forbidden, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(text, name+"-safe") {
|
||||||
|
t.Fatalf("resolved pipeline summary does not retain safe option for %q: %s", name, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, content := range []string{
|
||||||
|
"chunk-reference-content", "extract-reference-content",
|
||||||
|
"merge-reference-content", "normalize-reference-content",
|
||||||
|
} {
|
||||||
|
if strings.Contains(text, content) {
|
||||||
|
t.Fatalf("resolved pipeline summary contains materialized reference content %q", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, safe := range []string{"[REDACTED]", "safe-reference-path", "safe-binding-source"} {
|
||||||
|
if !strings.Contains(text, safe) {
|
||||||
|
t.Fatalf("resolved pipeline summary does not retain %q: %s", safe, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.Input.Options["safe"] = "mutated"
|
||||||
|
nested := payload.Input.Options["nested"].([]any)[0].([]any)[0].(map[string]any)
|
||||||
|
nested["neighbor"] = "mutated"
|
||||||
|
payload.ChunkReferences.ReferenceSet.Slots["safe-slot"].Items[0].Content[0] = 'X'
|
||||||
|
payload.ValidatorChains[0].Validators[0].Binding.Options["safe"] = "mutated"
|
||||||
|
|
||||||
|
assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.Input, "input")
|
||||||
|
assertRedactionTestBindingUnchanged(t, effective.Config.Pipelines["redaction-test"].Input, "input")
|
||||||
|
assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.ValidatorChains[0].Validators[0].Binding, "resolved-validator")
|
||||||
|
if got := string(effective.ResolvedPipeline.ChunkReferences.ReferenceSet.Slots["safe-slot"].Items[0].Content); got != "chunk-reference-content" {
|
||||||
|
t.Fatalf("source reference content mutated through redacted payload: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedactedEffectiveConfigPayloadDoesNotAliasSource(t *testing.T) {
|
||||||
|
binding := redactionTestBinding("effective")
|
||||||
|
effective := EffectiveConfig{
|
||||||
|
Config: Config{Pipelines: map[string]pipeline.PipelineProfile{
|
||||||
|
"redaction-test": {Input: binding},
|
||||||
|
}},
|
||||||
|
ResolvedPipeline: pipeline.ResolvedPipeline{Input: binding},
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := effective.RedactedSummaryPayload().(EffectiveConfig)
|
||||||
|
payload.Config.Pipelines["redaction-test"].Input.Options["safe"] = "mutated"
|
||||||
|
payload.ResolvedPipeline.Input.Options["safe"] = "mutated"
|
||||||
|
|
||||||
|
assertRedactionTestBindingUnchanged(t, effective.Config.Pipelines["redaction-test"].Input, "effective")
|
||||||
|
assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.Input, "effective")
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactionTestBinding(name string) pipeline.ModuleBinding {
|
||||||
|
return pipeline.ModuleBinding{
|
||||||
|
Module: "safe-" + name,
|
||||||
|
Options: map[string]any{
|
||||||
|
"api_key": name + "-secret",
|
||||||
|
"safe": name + "-safe",
|
||||||
|
"nested": []any{[]any{map[string]any{
|
||||||
|
"password": name + "-nested-secret",
|
||||||
|
"neighbor": name + "-nested-safe",
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactionTestReferenceTarget(stage pipeline.ModuleStage, laneID, content string) pipeline.ResolvedReferenceTarget {
|
||||||
|
return pipeline.ResolvedReferenceTarget{
|
||||||
|
Stage: stage,
|
||||||
|
LaneID: laneID,
|
||||||
|
Module: "safe-reference-module",
|
||||||
|
Bindings: []pipeline.ReferenceBinding{{
|
||||||
|
Stage: stage,
|
||||||
|
LaneID: laneID,
|
||||||
|
SlotName: "safe-slot",
|
||||||
|
Source: "safe-reference-path",
|
||||||
|
BindingSource: "safe-binding-source",
|
||||||
|
}},
|
||||||
|
ReferenceSet: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||||
|
"safe-slot": {
|
||||||
|
Slot: contracts.ReferenceSlot{Name: "safe-slot"},
|
||||||
|
Items: []contracts.ReferenceItem{{
|
||||||
|
SlotName: "safe-slot",
|
||||||
|
Content: []byte(content),
|
||||||
|
Digest: "sha256:safe-reference-digest",
|
||||||
|
BindingSource: "safe-binding-source",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertRedactionTestBindingUnchanged(t *testing.T, binding pipeline.ModuleBinding, name string) {
|
||||||
|
t.Helper()
|
||||||
|
if got := binding.Options["safe"]; got != name+"-safe" {
|
||||||
|
t.Fatalf("source safe option = %v, want %q", got, name+"-safe")
|
||||||
|
}
|
||||||
|
nested := binding.Options["nested"].([]any)[0].([]any)[0].(map[string]any)
|
||||||
|
if got := nested["neighbor"]; got != name+"-nested-safe" {
|
||||||
|
t.Fatalf("source nested safe option = %v, want %q", got, name+"-nested-safe")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAllocateCreatesRestrictiveSummaryAndTrace(t *testing.T) {
|
func TestAllocateCreatesRestrictiveSummaryAndTrace(t *testing.T) {
|
||||||
@@ -92,7 +93,7 @@ func TestSummaryWriterWritesEverySummaryArtifact(t *testing.T) {
|
|||||||
if err := summary.WriteRedactedEffectiveConfig(testRedactedSummaryPayload{}); err != nil {
|
if err := summary.WriteRedactedEffectiveConfig(testRedactedSummaryPayload{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := summary.WriteResolvedPipeline(map[string]any{}); err != nil {
|
if err := summary.WriteResolvedPipeline(testRedactedResolvedPipelinePayload{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := summary.WriteResolvedReferences(nil); err != nil {
|
if err := summary.WriteResolvedReferences(nil); err != nil {
|
||||||
@@ -156,3 +157,9 @@ type testRedactedSummaryPayload struct{}
|
|||||||
func (testRedactedSummaryPayload) RedactedSummaryPayload() any {
|
func (testRedactedSummaryPayload) RedactedSummaryPayload() any {
|
||||||
return map[string]any{"redacted": true}
|
return map[string]any{"redacted": true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testRedactedResolvedPipelinePayload struct{}
|
||||||
|
|
||||||
|
func (testRedactedResolvedPipelinePayload) RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline {
|
||||||
|
return pipeline.ResolvedPipeline{ID: "redacted"}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/fileio"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/fileio"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -24,6 +25,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RedactedSummaryPayload interface{ RedactedSummaryPayload() any }
|
type RedactedSummaryPayload interface{ RedactedSummaryPayload() any }
|
||||||
|
type RedactedResolvedPipelinePayload interface {
|
||||||
|
RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline
|
||||||
|
}
|
||||||
type Invocation struct {
|
type Invocation struct {
|
||||||
Operation string `json:"operation"`
|
Operation string `json:"operation"`
|
||||||
PipelineID string `json:"pipeline_id,omitempty"`
|
PipelineID string `json:"pipeline_id,omitempty"`
|
||||||
@@ -71,8 +75,11 @@ func (w *SummaryWriter) WriteRedactedEffectiveConfig(payload RedactedSummaryPayl
|
|||||||
}
|
}
|
||||||
return w.WriteJSON(ArtifactEffectiveConfig, payload.RedactedSummaryPayload())
|
return w.WriteJSON(ArtifactEffectiveConfig, payload.RedactedSummaryPayload())
|
||||||
}
|
}
|
||||||
func (w *SummaryWriter) WriteResolvedPipeline(v any) error {
|
func (w *SummaryWriter) WriteResolvedPipeline(payload RedactedResolvedPipelinePayload) error {
|
||||||
return w.WriteJSON(ArtifactResolvedPipeline, v)
|
if payload == nil {
|
||||||
|
return fmt.Errorf("redacted resolved pipeline payload must not be nil")
|
||||||
|
}
|
||||||
|
return w.WriteJSON(ArtifactResolvedPipeline, payload.RedactedResolvedPipelinePayload())
|
||||||
}
|
}
|
||||||
func (w *SummaryWriter) WriteResolvedReferences(v any) error {
|
func (w *SummaryWriter) WriteResolvedReferences(v any) error {
|
||||||
return w.WriteJSON(ArtifactResolvedReferences, v)
|
return w.WriteJSON(ArtifactResolvedReferences, v)
|
||||||
|
|||||||
Reference in New Issue
Block a user