From ee9719d84571d093d267fdf8ea2ad05fb24b9771 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 14 Jan 2026 18:29:58 +0100 Subject: [PATCH 1/5] feat(attestation): Include attestation output automatically on GitHub and GitLab reports Signed-off-by: Javier Rodriguez --- app/cli/cmd/attestation_push.go | 31 ++++++++++++++----- app/cli/cmd/attestation_status.go | 10 +++--- app/cli/documentation/cli-reference.mdx | 1 + app/cli/pkg/action/attestation_push.go | 2 +- app/cli/pkg/action/attestation_status.go | 2 ++ pkg/attestation/crafter/runner.go | 4 +++ .../crafter/runners/azurepipeline.go | 4 +++ .../crafter/runners/circleci_build.go | 4 +++ .../crafter/runners/daggerpipeline.go | 4 +++ pkg/attestation/crafter/runners/generic.go | 6 +++- .../crafter/runners/githubaction.go | 28 +++++++++++++++++ .../crafter/runners/gitlabpipeline.go | 12 +++++++ pkg/attestation/crafter/runners/jenkinsjob.go | 4 +++ .../crafter/runners/teamcitypipeline.go | 4 +++ .../crafter/runners/tektonpipeline.go | 4 +++ 15 files changed, 105 insertions(+), 15 deletions(-) diff --git a/app/cli/cmd/attestation_push.go b/app/cli/cmd/attestation_push.go index a5954a9b8..5e518ae13 100644 --- a/app/cli/cmd/attestation_push.go +++ b/app/cli/cmd/attestation_push.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/spf13/cobra" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -37,6 +38,7 @@ func newAttestationPushCmd() *cobra.Command { signServerAuthCertPath string signServerAuthCertPass string bypassPolicyCheck bool + deactivateCIReport bool ) cmd := &cobra.Command{ @@ -106,15 +108,27 @@ func newAttestationPushCmd() *cobra.Command { res.Status.Digest = res.Digest - // If we are returning the json format, we also want to render the attestation table as one property so it can also be consumed - if flagOutputFormat == output.FormatJSON { - // Render the attestation status to a string - buf := &bytes.Buffer{} - if err := fullStatusTableWithWriter(res.Status, buf); err != nil { - return fmt.Errorf("failed to render output: %w", err) - } + // Render the attestation status to a string + buf := &bytes.Buffer{} + if err := fullStatusTableWithWriter(res.Status, buf); err != nil { + return fmt.Errorf("failed to render output: %w", err) + } - res.Status.TerminalOutput = buf.Bytes() + res.Status.TerminalOutput = buf.Bytes() + + // Report to CI/CD platform if supported and not disabled + if !deactivateCIReport && !res.Status.DryRun && res.Status.RunnerContext != nil { + if err := res.Status.RunnerContext.RawRunner.Report(res.Status.TerminalOutput); err != nil { + logger.Warn().Err(err).Msg("failed to write CI/CD platform report") + } else { + // Log success message based on runner type + switch res.Status.RunnerContext.RawRunner.ID() { + case schemaapi.CraftingSchema_Runner_GITHUB_ACTION: + logger.Info().Msg("attestation report written to GitHub step summary") + case schemaapi.CraftingSchema_Runner_GITLAB_PIPELINE: + logger.Info().Msg("attestation report written to chainloop-attestation-report.txt artifact") + } + } } // In TABLE format, we render the attestation status @@ -160,6 +174,7 @@ func newAttestationPushCmd() *cobra.Command { cmd.Flags().StringVar(&signServerAuthCertPath, "signserver-client-cert", "", "path to client certificate in PEM format for authenticated SignServer TLS connection") cmd.Flags().StringVar(&signServerAuthCertPass, "signserver-client-pass", "", "certificate passphrase for authenticated SignServer TLS connection") cmd.Flags().BoolVar(&bypassPolicyCheck, exceptionFlagName, false, "do not fail this command on policy violations enforcement") + cmd.Flags().BoolVar(&deactivateCIReport, "deactivate-ci-report", false, "deactivate automatic attestation report to CI/CD platform") return cmd } diff --git a/app/cli/cmd/attestation_status.go b/app/cli/cmd/attestation_status.go index 2d8fbfc39..724dc3794 100644 --- a/app/cli/cmd/attestation_status.go +++ b/app/cli/cmd/attestation_status.go @@ -126,13 +126,13 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, w io.W gt.AppendRow(table.Row{"Timestamp Authority", status.TimestampAuthority}) } - var blockingColor text.Color - var blockingText = action.PolicyViolationBlockingStrategyAdvisory + var blockingText string if status.MustBlockOnPolicyViolations { - blockingColor = text.FgHiYellow - blockingText = action.PolicyViolationBlockingStrategyEnforced + blockingText = text.FgHiYellow.Sprint(action.PolicyViolationBlockingStrategyEnforced) + } else { + blockingText = action.PolicyViolationBlockingStrategyAdvisory } - gt.AppendRow(table.Row{"Policy violation strategy", blockingColor.Sprint(blockingText)}) + gt.AppendRow(table.Row{"Policy violation strategy", blockingText}) evs := status.PolicyEvaluations[chainloop.AttPolicyEvaluation] if len(evs) > 0 { diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 8150630cb..b78a77027 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -341,6 +341,7 @@ Options --annotation strings additional annotation in the format of key=value --attestation-id string Unique identifier of the in-progress attestation --bundle string output a Sigstore bundle to the provided path +--deactivate-ci-report deactivate automatic attestation report to CI/CD platform --exception-bypass-policy-check do not fail this command on policy violations enforcement -h, --help help for push -k, --key string reference (path or env variable name) to the cosign or KMS key that will be used to sign the attestation diff --git a/app/cli/pkg/action/attestation_push.go b/app/cli/pkg/action/attestation_push.go index 30c597c77..6af5a0562 100644 --- a/app/cli/pkg/action/attestation_push.go +++ b/app/cli/pkg/action/attestation_push.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2025 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/app/cli/pkg/action/attestation_status.go b/app/cli/pkg/action/attestation_status.go index 9e1abbc6d..cbeb57e27 100644 --- a/app/cli/pkg/action/attestation_status.go +++ b/app/cli/pkg/action/attestation_status.go @@ -66,6 +66,7 @@ type AttestationStatusResult struct { type AttestationResultRunnerContext struct { EnvVars map[string]string JobURL, RunnerType string + RawRunner crafter.SupportedRunner } type AttestationStatusWorkflowMeta struct { @@ -202,6 +203,7 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string, EnvVars: runnerEnvVars, RunnerType: att.RunnerType.String(), JobURL: att.RunnerUrl, + RawRunner: c.Runner, } return res, nil diff --git a/pkg/attestation/crafter/runner.go b/pkg/attestation/crafter/runner.go index b47fa11db..0321ac0a4 100644 --- a/pkg/attestation/crafter/runner.go +++ b/pkg/attestation/crafter/runner.go @@ -58,6 +58,10 @@ type SupportedRunner interface { // Returns nil if verification is not supported or not applicable for this runner. // Non-blocking: errors are logged and returned as unavailable status. VerifyCommitSignature(ctx context.Context, commitHash string) *commitverification.CommitVerification + + // Report writes attestation table output to platform-specific location. + // Returns nil if platform doesn't support reporting. + Report(tableOutput []byte) error } type RunnerM map[schemaapi.CraftingSchema_Runner_RunnerType]SupportedRunner diff --git a/pkg/attestation/crafter/runners/azurepipeline.go b/pkg/attestation/crafter/runners/azurepipeline.go index cf07010d5..9963c0412 100644 --- a/pkg/attestation/crafter/runners/azurepipeline.go +++ b/pkg/attestation/crafter/runners/azurepipeline.go @@ -101,3 +101,7 @@ func (r *AzurePipeline) Environment() RunnerEnvironment { func (r *AzurePipeline) VerifyCommitSignature(_ context.Context, _ string) *commitverification.CommitVerification { return nil // Not supported for this runner } + +func (r *AzurePipeline) Report(_ []byte) error { + return nil +} diff --git a/pkg/attestation/crafter/runners/circleci_build.go b/pkg/attestation/crafter/runners/circleci_build.go index 4a6ea5518..cd9453646 100644 --- a/pkg/attestation/crafter/runners/circleci_build.go +++ b/pkg/attestation/crafter/runners/circleci_build.go @@ -81,3 +81,7 @@ func (r *CircleCIBuild) Environment() RunnerEnvironment { func (r *CircleCIBuild) VerifyCommitSignature(_ context.Context, _ string) *commitverification.CommitVerification { return nil // Not supported for this runner } + +func (r *CircleCIBuild) Report(_ []byte) error { + return nil +} diff --git a/pkg/attestation/crafter/runners/daggerpipeline.go b/pkg/attestation/crafter/runners/daggerpipeline.go index 7ece73d89..5aa61fdad 100644 --- a/pkg/attestation/crafter/runners/daggerpipeline.go +++ b/pkg/attestation/crafter/runners/daggerpipeline.go @@ -181,3 +181,7 @@ func (r *DaggerPipeline) verifyCommitViaGitLab(ctx context.Context, commitHash s // Call GitLab API to verify commit return commitverification.VerifyGitLabCommit(ctx, baseURL, projectPath, commitHash, token, r.logger) } + +func (r *DaggerPipeline) Report(_ []byte) error { + return nil +} diff --git a/pkg/attestation/crafter/runners/generic.go b/pkg/attestation/crafter/runners/generic.go index ec99cf79c..b8d6e85fb 100644 --- a/pkg/attestation/crafter/runners/generic.go +++ b/pkg/attestation/crafter/runners/generic.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -65,3 +65,7 @@ func (r *Generic) Environment() RunnerEnvironment { func (r *Generic) VerifyCommitSignature(_ context.Context, _ string) *commitverification.CommitVerification { return nil // Not supported for this runner } + +func (r *Generic) Report(_ []byte) error { + return nil +} diff --git a/pkg/attestation/crafter/runners/githubaction.go b/pkg/attestation/crafter/runners/githubaction.go index 0bfd30c3a..5f4a19fb1 100644 --- a/pkg/attestation/crafter/runners/githubaction.go +++ b/pkg/attestation/crafter/runners/githubaction.go @@ -166,3 +166,31 @@ func (r *GitHubAction) VerifyCommitSignature(ctx context.Context, commitHash str // Call GitHub API to verify commit return commitverification.VerifyGitHubCommit(ctx, parts[0], parts[1], commitHash, token, r.logger) } + +// Report writes attestation table output to GitHub Step Summary +func (r *GitHubAction) Report(tableOutput []byte) error { + summaryFile := os.Getenv("GITHUB_STEP_SUMMARY") + if summaryFile == "" { + return fmt.Errorf("GITHUB_STEP_SUMMARY environment variable not set") + } + + // Wrap table output in markdown code block + var content strings.Builder + content.WriteString("## Chainloop Attestation Report\n\n") + content.WriteString("```\n") + content.Write(tableOutput) + content.WriteString("```\n") + + // Append to GITHUB_STEP_SUMMARY file + f, err := os.OpenFile(summaryFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open GITHUB_STEP_SUMMARY: %w", err) + } + defer f.Close() + + if _, err := f.WriteString(content.String()); err != nil { + return fmt.Errorf("failed to write to GITHUB_STEP_SUMMARY: %w", err) + } + + return nil +} diff --git a/pkg/attestation/crafter/runners/gitlabpipeline.go b/pkg/attestation/crafter/runners/gitlabpipeline.go index ab2cbec1f..2dfe22ff5 100644 --- a/pkg/attestation/crafter/runners/gitlabpipeline.go +++ b/pkg/attestation/crafter/runners/gitlabpipeline.go @@ -17,6 +17,7 @@ package runners import ( "context" + "fmt" "os" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" @@ -138,3 +139,14 @@ func (r *GitlabPipeline) VerifyCommitSignature(ctx context.Context, commitHash s // Call GitLab API to verify commit return commitverification.VerifyGitLabCommit(ctx, baseURL, projectPath, commitHash, token, r.logger) } + +// Report writes attestation table output as text artifact +func (r *GitlabPipeline) Report(tableOutput []byte) error { + artifactFile := "chainloop-attestation-report.txt" + + if err := os.WriteFile(artifactFile, tableOutput, 0644); err != nil { + return fmt.Errorf("failed to write attestation report: %w", err) + } + + return nil +} diff --git a/pkg/attestation/crafter/runners/jenkinsjob.go b/pkg/attestation/crafter/runners/jenkinsjob.go index 66df53b5c..a4a4a3758 100644 --- a/pkg/attestation/crafter/runners/jenkinsjob.go +++ b/pkg/attestation/crafter/runners/jenkinsjob.go @@ -88,3 +88,7 @@ func (r *JenkinsJob) Environment() RunnerEnvironment { func (r *JenkinsJob) VerifyCommitSignature(_ context.Context, _ string) *commitverification.CommitVerification { return nil // Not supported for this runner } + +func (r *JenkinsJob) Report(_ []byte) error { + return nil +} diff --git a/pkg/attestation/crafter/runners/teamcitypipeline.go b/pkg/attestation/crafter/runners/teamcitypipeline.go index 3ba4996ee..40da7d128 100644 --- a/pkg/attestation/crafter/runners/teamcitypipeline.go +++ b/pkg/attestation/crafter/runners/teamcitypipeline.go @@ -74,3 +74,7 @@ func (r *TeamCityPipeline) Environment() RunnerEnvironment { func (r *TeamCityPipeline) VerifyCommitSignature(_ context.Context, _ string) *commitverification.CommitVerification { return nil // Not supported for this runner } + +func (r *TeamCityPipeline) Report(_ []byte) error { + return nil +} diff --git a/pkg/attestation/crafter/runners/tektonpipeline.go b/pkg/attestation/crafter/runners/tektonpipeline.go index 9509f0b5b..922f00fa8 100644 --- a/pkg/attestation/crafter/runners/tektonpipeline.go +++ b/pkg/attestation/crafter/runners/tektonpipeline.go @@ -70,3 +70,7 @@ func (r *TektonPipeline) Environment() RunnerEnvironment { func (r *TektonPipeline) VerifyCommitSignature(_ context.Context, _ string) *commitverification.CommitVerification { return nil // Not supported for this runner } + +func (r *TektonPipeline) Report(_ []byte) error { + return nil +} From a7577e6d5a1435412f6a7c60e67fc07817fedf35 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 14 Jan 2026 18:40:43 +0100 Subject: [PATCH 2/5] fix write permissions Signed-off-by: Javier Rodriguez --- pkg/attestation/crafter/runners/gitlabpipeline.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/attestation/crafter/runners/gitlabpipeline.go b/pkg/attestation/crafter/runners/gitlabpipeline.go index 2dfe22ff5..f10bac19e 100644 --- a/pkg/attestation/crafter/runners/gitlabpipeline.go +++ b/pkg/attestation/crafter/runners/gitlabpipeline.go @@ -144,7 +144,7 @@ func (r *GitlabPipeline) VerifyCommitSignature(ctx context.Context, commitHash s func (r *GitlabPipeline) Report(tableOutput []byte) error { artifactFile := "chainloop-attestation-report.txt" - if err := os.WriteFile(artifactFile, tableOutput, 0644); err != nil { + if err := os.WriteFile(artifactFile, tableOutput, 0600); err != nil { return fmt.Errorf("failed to write attestation report: %w", err) } From 8b8f48aa423fce0e96b6986508a6d2d8a4e740f1 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 14 Jan 2026 18:43:11 +0100 Subject: [PATCH 3/5] add info on gitlab pipeline Signed-off-by: Javier Rodriguez --- pkg/attestation/crafter/runners/gitlabpipeline.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/attestation/crafter/runners/gitlabpipeline.go b/pkg/attestation/crafter/runners/gitlabpipeline.go index f10bac19e..e1fac8495 100644 --- a/pkg/attestation/crafter/runners/gitlabpipeline.go +++ b/pkg/attestation/crafter/runners/gitlabpipeline.go @@ -148,5 +148,13 @@ func (r *GitlabPipeline) Report(tableOutput []byte) error { return fmt.Errorf("failed to write attestation report: %w", err) } + // Log instruction for GitLab CI configuration + r.logger.Info().Msgf("Attestation report written to %s", artifactFile) + r.logger.Info().Msg("To view in GitLab CI, add this to your job configuration:") + r.logger.Info().Msg("artifacts:") + r.logger.Info().Msg(" paths:") + r.logger.Info().Msgf(" - %s", artifactFile) + r.logger.Info().Msg(" expire_in: 30 days") + return nil } From 3aae1e5836ac47115757f4294cc63619e1a41997 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 15 Jan 2026 09:56:02 +0100 Subject: [PATCH 4/5] cleanup ANSI codes before output to GH and GitLab Signed-off-by: Javier Rodriguez --- app/cli/cmd/attestation_push.go | 4 +++- app/cli/cmd/attestation_status.go | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/cli/cmd/attestation_push.go b/app/cli/cmd/attestation_push.go index 5e518ae13..77c978989 100644 --- a/app/cli/cmd/attestation_push.go +++ b/app/cli/cmd/attestation_push.go @@ -118,7 +118,9 @@ func newAttestationPushCmd() *cobra.Command { // Report to CI/CD platform if supported and not disabled if !deactivateCIReport && !res.Status.DryRun && res.Status.RunnerContext != nil { - if err := res.Status.RunnerContext.RawRunner.Report(res.Status.TerminalOutput); err != nil { + // Clean up possible ANSI characters from the output + sanitizedOutput := removeAnsiCharactersFromBytes(res.Status.TerminalOutput) + if err := res.Status.RunnerContext.RawRunner.Report(sanitizedOutput); err != nil { logger.Warn().Err(err).Msg("failed to write CI/CD platform report") } else { // Log success message based on runner type diff --git a/app/cli/cmd/attestation_status.go b/app/cli/cmd/attestation_status.go index 724dc3794..36afb8d77 100644 --- a/app/cli/cmd/attestation_status.go +++ b/app/cli/cmd/attestation_status.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "os" + "regexp" "slices" "strings" "time" @@ -126,13 +127,13 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, w io.W gt.AppendRow(table.Row{"Timestamp Authority", status.TimestampAuthority}) } - var blockingText string + var blockingColor text.Color + var blockingText = action.PolicyViolationBlockingStrategyAdvisory if status.MustBlockOnPolicyViolations { - blockingText = text.FgHiYellow.Sprint(action.PolicyViolationBlockingStrategyEnforced) - } else { - blockingText = action.PolicyViolationBlockingStrategyAdvisory + blockingColor = text.FgHiYellow + blockingText = action.PolicyViolationBlockingStrategyEnforced } - gt.AppendRow(table.Row{"Policy violation strategy", blockingText}) + gt.AppendRow(table.Row{"Policy violation strategy", blockingColor.Sprint(blockingText)}) evs := status.PolicyEvaluations[chainloop.AttPolicyEvaluation] if len(evs) > 0 { @@ -302,3 +303,11 @@ func versionStringAttFinal(p *action.ProjectVersion) string { return p.Version } + +// removeAnsiCharactersFromBytes removes ANSI escape codes from strings +// Credits to: https://github.com/acarl005/stripansi +func removeAnsiCharactersFromBytes(input []byte) []byte { + const ansiPattern = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" + re := regexp.MustCompile(ansiPattern) + return re.ReplaceAll(input, []byte("")) +} From 7056d63840c89fabb8eaa135f8c41a956d2d5183 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 15 Jan 2026 09:56:33 +0100 Subject: [PATCH 5/5] change method signature comment Signed-off-by: Javier Rodriguez --- app/cli/cmd/attestation_status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/cli/cmd/attestation_status.go b/app/cli/cmd/attestation_status.go index 36afb8d77..b509c46c4 100644 --- a/app/cli/cmd/attestation_status.go +++ b/app/cli/cmd/attestation_status.go @@ -304,7 +304,7 @@ func versionStringAttFinal(p *action.ProjectVersion) string { return p.Version } -// removeAnsiCharactersFromBytes removes ANSI escape codes from strings +// removeAnsiCharactersFromBytes removes ANSI escape codes from bytes slices. // Credits to: https://github.com/acarl005/stripansi func removeAnsiCharactersFromBytes(input []byte) []byte { const ansiPattern = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"