Skip to content

Conversation

@arewm
Copy link
Contributor

@arewm arewm commented Jan 20, 2026

User description

Add support for generating VSAs in different formats to enable flexible signing workflows. The new --attestation-format flag accepts two values:

  • "dsse" (default): Generates complete DSSE envelope with signature (existing behavior, requires --vsa-signing-key)
  • "predicate": Generates raw VSA predicate JSON without signature (new capability, enables downstream signing with correct subject)

This addresses the challenge in release pipelines where images are validated before being pushed to destination registries. With predicate format, validation can generate unsigned VSAs that are later signed with the correct image location after the push completes.

The implementation maintains backward compatibility by defaulting to "dsse" format and reuses existing VSA generation functions. Format validation ensures only supported values are accepted.

Updated verify-conforma-konflux-ta task to support VSA generation with parameters for format selection, signing key configuration, and trusted artifact storage integration.

Assisted-by: Claude Code (Sonnet 4.5)


PR Type

Enhancement


Description

  • Add --vsa-format flag supporting "dsse" (signed envelope) and "predicate" (raw JSON) formats

  • Enable unsigned VSA predicate generation for downstream signing workflows

  • Implement format validation with appropriate signing key requirements

  • Update verify-conforma-konflux-ta task with VSA parameters and trusted artifact integration


Diagram Walkthrough

flowchart LR
  A["VSA Generation"] --> B{VSA Format}
  B -->|"dsse"| C["DSSE Envelope<br/>with Signature"]
  B -->|"predicate"| D["Raw Predicate<br/>JSON"]
  C --> E["Upload to Storage"]
  D --> F["Downstream Signing"]
  E --> G["Signed Attestation"]
  F --> G
Loading

File Walkthrough

Relevant files
Enhancement
image.go
Implement VSA format flag with dual code paths                     

cmd/validate/image.go

  • Add vsaFormat field to command data structure with default value
    "dsse"
  • Implement format validation logic accepting only "dsse" and
    "predicate" values
  • Add conditional logic branching: DSSE path uses existing service-based
    approach, predicate path generates raw JSON
  • Implement extractLocalPath() helper function to parse VSA upload
    specifications
  • Add format-specific signing key requirement validation
+114/-54
verify-conforma-konflux-ta.yaml
Extend task with VSA parameters and trusted artifact support

tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml

  • Add new task parameters: ENABLE_VSA, VSA_FORMAT, VSA_SIGNING_KEY,
    VSA_UPLOAD, ociStorage
  • Add new task results: VSA_GENERATED, sourceDataArtifact
  • Convert validate step from command/args to script format for
    conditional VSA argument construction
  • Implement conditional logic to add VSA flags only when enabled
  • Add validation requiring signing key for DSSE format
  • Add new create-trusted-artifact step for storing VSA files in OCI
    storage
+110/-42
Tests
image_test.go
Add comprehensive VSA format flag tests                                   

cmd/validate/image_test.go

  • Add test for DSSE format generation with signing key
  • Add test for predicate format generation without signing key
  • Add test validating rejection of invalid format values
  • Add test ensuring DSSE format requires signing key
  • Add test confirming predicate format works without signing key
+199/-0 
Documentation
ec_validate_image.adoc
Document --vsa-format flag in CLI reference                           

docs/modules/ROOT/pages/ec_validate_image.adoc

  • Document new --vsa-format flag with description of supported values
  • Clarify format options: "dsse" for signed envelope, "predicate" for
    raw JSON
  • Set default value to "dsse" for backward compatibility
+1/-0     

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 20, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Arbitrary file write

Description: User-controlled --vsa-upload values with a local@... spec are parsed by extractLocalPath()
without validation/canonicalization and then used as TempDirPrefix for predicate output,
enabling writes to arbitrary filesystem locations (e.g., --vsa-upload local@/etc or
local@../../..) when the CLI runs with elevated permissions or in sensitive environments
(CI runners/containers).
image.go [562-704]

Referred Code
			// Extract directory from --vsa-upload (e.g., "local@/path" -> "/path")
			uploadDir := extractLocalPath(data.vsaUpload)

			writer := &vsa.Writer{
				FS:            utils.FS(cmd.Context()),
				TempDirPrefix: uploadDir,
				FilePerm:      0o600,
			}

			predicatePath, err := vsa.GenerateAndWritePredicate(cmd.Context(), generator, writer)
			if err != nil {
				log.Errorf("Failed to generate predicate for %s: %v", comp.ContainerImage, err)
				continue
			}

			log.Infof("[VSA] Generated predicate for %s at %s", comp.ContainerImage, predicatePath)
		}
	}
}

if data.strict && !report.Success {


 ... (clipped 122 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Incomplete audit context: The new VSA generation/upload logs record actions but do not clearly include actor/user
identity or other audit context needed to reconstruct who performed uploads and with what
authorization.

Referred Code
	log.Errorf("Failed to process VSAs: %v", err)
	// Don't return error here, continue with the rest of the command
} else {
	// Upload VSAs to configured storage backends
	if len(data.vsaUpload) > 0 {
		log.Infof("[VSA] Starting upload to %d storage backend(s)", len(data.vsaUpload))

		// Upload component VSA envelopes
		for imageRef, envelopePath := range vsaResult.ComponentEnvelopes {
			uploadErr := vsa.UploadVSAEnvelope(cmd.Context(), envelopePath, data.vsaUpload, signer)
			if uploadErr != nil {
				log.Errorf("[VSA] Upload failed for component %s: %v", imageRef, uploadErr)
			} else {
				log.Infof("[VSA] Uploaded Component VSA")
			}
		}

		// Upload snapshot VSA envelope if it exists
		if vsaResult.SnapshotEnvelope != "" {
			uploadErr := vsa.UploadVSAEnvelope(cmd.Context(), vsaResult.SnapshotEnvelope, data.vsaUpload, signer)
			if uploadErr != nil {


 ... (clipped 16 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Errors not propagated: VSA processing/upload failures are logged but intentionally do not fail the command, which
may make pipelines silently proceed without required VSAs unless external checks enforce
success.

Referred Code
if err != nil {
	log.Errorf("Failed to process VSAs: %v", err)
	// Don't return error here, continue with the rest of the command
} else {
	// Upload VSAs to configured storage backends
	if len(data.vsaUpload) > 0 {
		log.Infof("[VSA] Starting upload to %d storage backend(s)", len(data.vsaUpload))

		// Upload component VSA envelopes
		for imageRef, envelopePath := range vsaResult.ComponentEnvelopes {
			uploadErr := vsa.UploadVSAEnvelope(cmd.Context(), envelopePath, data.vsaUpload, signer)
			if uploadErr != nil {
				log.Errorf("[VSA] Upload failed for component %s: %v", imageRef, uploadErr)
			} else {
				log.Infof("[VSA] Uploaded Component VSA")
			}
		}

		// Upload snapshot VSA envelope if it exists
		if vsaResult.SnapshotEnvelope != "" {
			uploadErr := vsa.UploadVSAEnvelope(cmd.Context(), vsaResult.SnapshotEnvelope, data.vsaUpload, signer)


 ... (clipped 18 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured log lines: The added VSA logs are emitted as free-form strings (e.g., upload/generation messages) and
it is unclear from the diff whether the logger provides structured/JSON output required
for auditing.

Referred Code
	log.Errorf("Failed to process VSAs: %v", err)
	// Don't return error here, continue with the rest of the command
} else {
	// Upload VSAs to configured storage backends
	if len(data.vsaUpload) > 0 {
		log.Infof("[VSA] Starting upload to %d storage backend(s)", len(data.vsaUpload))

		// Upload component VSA envelopes
		for imageRef, envelopePath := range vsaResult.ComponentEnvelopes {
			uploadErr := vsa.UploadVSAEnvelope(cmd.Context(), envelopePath, data.vsaUpload, signer)
			if uploadErr != nil {
				log.Errorf("[VSA] Upload failed for component %s: %v", imageRef, uploadErr)
			} else {
				log.Infof("[VSA] Uploaded Component VSA")
			}
		}

		// Upload snapshot VSA envelope if it exists
		if vsaResult.SnapshotEnvelope != "" {
			uploadErr := vsa.UploadVSAEnvelope(cmd.Context(), vsaResult.SnapshotEnvelope, data.vsaUpload, signer)
			if uploadErr != nil {


 ... (clipped 39 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Path input not validated: The new predicate-writing path derives a filesystem directory from --vsa-upload (and falls
back to a fixed /tmp/vsa) without validating/sanitizing the path, which may allow
unintended write locations depending on runtime context.

Referred Code
			// Extract directory from --vsa-upload (e.g., "local@/path" -> "/path")
			uploadDir := extractLocalPath(data.vsaUpload)

			writer := &vsa.Writer{
				FS:            utils.FS(cmd.Context()),
				TempDirPrefix: uploadDir,
				FilePerm:      0o600,
			}

			predicatePath, err := vsa.GenerateAndWritePredicate(cmd.Context(), generator, writer)
			if err != nil {
				log.Errorf("Failed to generate predicate for %s: %v", comp.ContainerImage, err)
				continue
			}

			log.Infof("[VSA] Generated predicate for %s at %s", comp.ContainerImage, predicatePath)
		}
	}
}

if data.strict && !report.Success {


 ... (clipped 122 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 20, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Return error on missing local path
Suggestion Impact:extractLocalPath was updated to return (string, error) and now errors if no local@ path is found; the caller was modified to handle the error and skip processing when extraction fails, preventing silent fallback to /tmp/vsa.

code diff:

@@ -560,7 +560,11 @@
 						generator := vsa.NewGenerator(report, comp, data.policySource, data.policy)
 
 						// Extract directory from --vsa-upload (e.g., "local@/path" -> "/path")
-						uploadDir := extractLocalPath(data.vsaUpload)
+						uploadDir, err := extractLocalPath(data.vsaUpload)
+						if err != nil {
+							log.Errorf("Failed to extract upload path: %v", err)
+							continue
+						}
 
 						writer := &vsa.Writer{
 							FS:            utils.FS(cmd.Context()),
@@ -693,14 +697,14 @@
 
 // extractLocalPath extracts the path from a vsa-upload spec
 // Parses "local@/path/to/dir" -> "/path/to/dir"
-// Returns "/tmp/vsa" if no valid local path is found
-func extractLocalPath(uploadSpecs []string) string {
+// Returns an error if no valid local path is found
+func extractLocalPath(uploadSpecs []string) (string, error) {
 	for _, spec := range uploadSpecs {
 		if strings.HasPrefix(spec, "local@") {
-			return strings.TrimPrefix(spec, "local@")
+			return strings.TrimPrefix(spec, "local@"), nil
 		}
 	}
-	return "/tmp/vsa"
+	return "", errors.New("--vsa-upload with a 'local@' destination is required for --vsa-format=predicate")
 }

Modify extractLocalPath to return an error instead of a default path when no
local@ destination is found, preventing silent data loss.

cmd/validate/image.go [697-704]

 // extractLocalPath extracts the path from a vsa-upload spec
 // Parses "local@/path/to/dir" -> "/path/to/dir"
-// Returns "/tmp/vsa" if no valid local path is found
-func extractLocalPath(uploadSpecs []string) string {
+// Returns an error if no valid local path is found
+func extractLocalPath(uploadSpecs []string) (string, error) {
 	for _, spec := range uploadSpecs {
 		if strings.HasPrefix(spec, "local@") {
-			return strings.TrimPrefix(spec, "local@")
+			return strings.TrimPrefix(spec, "local@"), nil
 		}
 	}
-	return "/tmp/vsa"
+	return "", fmt.Errorf("--vsa-upload with a 'local@' destination is required for --vsa-format=predicate")
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that defaulting to a temporary path can lead to silent data loss. Changing the function to return an error enforces explicit path configuration, making the CLI behavior more robust and preventing accidental loss of generated artifacts.

Medium
General
Remove redundant CLI argument validation

Remove the redundant check for VSA_SIGNING_KEY from the Tekton task script, as
this validation is already handled by the ec CLI.

tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml [324-340]

 # Add VSA arguments if enabled
 if [[ "$(params.ENABLE_VSA)" == "true" ]]; then
   EC_ARGS+=(--vsa --vsa-format=$(params.VSA_FORMAT))
 
   if [[ "$(params.VSA_FORMAT)" == "dsse" ]]; then
-    if [[ -z "$(params.VSA_SIGNING_KEY)" ]]; then
-      echo "ERROR: VSA_SIGNING_KEY required for format=dsse"
-      exit 1
-    fi
     EC_ARGS+=(--vsa-signing-key "$(params.VSA_SIGNING_KEY)")
   fi
 
   EC_ARGS+=(--vsa-upload "$(params.VSA_UPLOAD)")
   echo "true" > $(results.VSA_GENERATED.path)
 else
   echo "false" > $(results.VSA_GENERATED.path)
 fi
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out that the validation for --vsa-signing-key is redundant, as the CLI already performs it. Removing this check from the Tekton task script simplifies it and improves maintainability by centralizing validation logic in the CLI.

Low
Downgrade upload message severity

Change the log level from error to warning when VSA files are generated but no
upload backends are specified, as this is not a fatal error.

cmd/validate/image.go [552-554]

 if totalFiles > 0 {
-    log.Errorf("[VSA] VSA files generated but not uploaded (no --vsa-upload backends specified)")
+    log.Warnf("[VSA] VSA files generated but not uploaded (no --vsa-upload backends specified)")
 }
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion correctly identifies that an error-level log is too severe for a non-fatal condition. Changing log.Errorf to log.Warnf is more appropriate since VSA files are generated locally and the program continues, making this a warning rather than an error.

Low
Simplify format validation
Suggestion Impact:The commit removed the original loop-based validation for the VSA format and replaced it with a simpler validation approach (using slices.Contains) and a switch-based dispatch on the format. Although it did not implement the exact suggested switch-for-validation snippet, it achieved the main intent: eliminating the manual loop/boolean flag and making the logic more readable.

code diff:

+			// Validate VSA configuration
+			if data.vsaEnabled {
+				if !slices.Contains([]string{"dsse", "predicate"}, data.attestationFormat) {
+					allErrors = errors.Join(allErrors, fmt.Errorf("invalid --attestation-format: %s (valid: dsse, predicate)", data.attestationFormat))
+				}
+				if data.attestationFormat == "dsse" && data.vsaSigningKey == "" {
+					allErrors = errors.Join(allErrors, fmt.Errorf("--vsa-signing-key required for --attestation-format=dsse"))
+				}
+				if data.attestationFormat == "predicate" && data.vsaSigningKey != "" {
+					log.Warn("--vsa-signing-key is ignored for --attestation-format=predicate")
+				}
 			}
 
 			return
@@ -458,123 +440,21 @@
 			}
 
 			if data.vsaEnabled {
-				// Validate format
-				validFormats := []string{"dsse", "predicate"}
-				formatValid := false
-				for _, f := range validFormats {
-					if data.vsaFormat == f {
-						formatValid = true
-						break
-					}
-				}
-				if !formatValid {
-					return fmt.Errorf("invalid --vsa-format: %s (valid: %s)",
-						data.vsaFormat, strings.Join(validFormats, ", "))
-				}
-
-				// Validate signing key requirement
-				if data.vsaFormat == "dsse" && data.vsaSigningKey == "" {
-					return fmt.Errorf("--vsa-signing-key required for --vsa-format=dsse")
-				}
-				if data.vsaFormat == "predicate" && data.vsaSigningKey != "" {
-					log.Warn("--vsa-signing-key is ignored for --vsa-format=predicate")
-				}
-
-				if data.vsaFormat == "dsse" {
-					// EXISTING PATH: Use service for DSSE envelopes
-					signer, err := vsa.NewSigner(cmd.Context(), data.vsaSigningKey, utils.FS(cmd.Context()))
-					if err != nil {
-						log.Error(err)
+				// Validate and get output directory
+				outputDir, err := validateAttestationOutputPath(data.attestationOutputDir)
+				if err != nil {
+					return fmt.Errorf("invalid attestation output directory: %w", err)
+				}
+
+				// Dispatch to appropriate method based on format
+				switch data.attestationFormat {
+				case "dsse":
+					if err := data.generateVSAsDSSE(cmd, report, outputDir); err != nil {
 						return err

Refactor the --vsa-format validation from a loop to a switch statement for
improved readability and conciseness.

cmd/validate/image.go [462-473]

-validFormats := []string{"dsse", "predicate"}
-formatValid := false
-for _, f := range validFormats {
-    if data.vsaFormat == f {
-        formatValid = true
-        break
-    }
-}
-if !formatValid {
-    return fmt.Errorf("invalid --vsa-format: %s (valid: %s)",
-        data.vsaFormat, strings.Join(validFormats, ", "))
+switch data.vsaFormat {
+case "dsse", "predicate":
+default:
+    return fmt.Errorf("invalid --vsa-format: %s (valid: dsse, predicate)", data.vsaFormat)
 }

[Suggestion processed]

Suggestion importance[1-10]: 4

__

Why: The suggestion improves code readability and conciseness by replacing a manual loop and boolean flag with a more idiomatic switch statement for validating the format. This is a good refactoring for maintainability.

Low
  • Update

@codecov
Copy link

codecov bot commented Jan 20, 2026

Codecov Report

❌ Patch coverage is 92.30769% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cmd/validate/image.go 91.20% 8 Missing ⚠️
Flag Coverage Δ
acceptance 55.72% <55.76%> (-0.26%) ⬇️
generative 18.98% <11.53%> (+0.01%) ⬆️
integration 28.18% <15.38%> (-0.27%) ⬇️
unit 68.02% <90.38%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
internal/validate/vsa/service.go 83.33% <100.00%> (+0.23%) ⬆️
internal/validate/vsa/vsa.go 63.61% <100.00%> (+0.60%) ⬆️
cmd/validate/image.go 91.24% <91.20%> (-0.11%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

break
}
}
if !formatValid {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving the validation to PreRunE.

PreRunE: func(cmd *cobra.Command, args []string) error {
        if data.vsaEnabled {
            if !slices.Contains([]string{"dsse", "predicate"}, data.vsaFormat) {
                return fmt.Errorf("invalid --vsa-format: %s (valid: dsse, predicate)", data.vsaFormat)
            }
            if data.vsaFormat == "dsse" && data.vsaSigningKey == "" {
                return fmt.Errorf("--vsa-signing-key required for --vsa-format=dsse")
            }
            if data.vsaFormat == "predicate" && data.vsaSigningKey != "" {
                log.Warn("--vsa-signing-key is ignored for --vsa-format=predicate")
            }
        }
        return nil
    }

@joejstuart
Copy link
Contributor

/ok-to-test

@arewm arewm changed the title feat: add --vsa-format flag to support multiple VSA output formats feat: add --attestation-format flag to support multiple VSA output formats Jan 20, 2026
@joejstuart
Copy link
Contributor

/ok-to-test

@joejstuart
Copy link
Contributor

Might need to rebase with upstream/main to fix the Konflux failure.

arewm added 4 commits January 20, 2026 17:31
Add support for generating VSAs in different formats to enable flexible
signing workflows. The new --vsa-format flag accepts two values:

- "dsse" (default): Generates complete DSSE envelope with signature
  (existing behavior, requires --vsa-signing-key)
- "predicate": Generates raw VSA predicate JSON without signature
  (new capability, enables downstream signing with correct subject)

This addresses the challenge in release pipelines where images are
validated before being pushed to destination registries. With predicate
format, validation can generate unsigned VSAs that are later signed
with the correct image location after the push completes.

The implementation maintains backward compatibility by defaulting to
"dsse" format and reuses existing VSA generation functions. Format
validation ensures only supported values are accepted.

Updated verify-conforma-konflux-ta task to support VSA generation with
parameters for format selection, signing key configuration, and trusted
artifact storage integration.

Assisted-by: Claude Code (Sonnet 4.5)
…apshots

Address PR feedback by improving error handling in extractLocalPath() and
fixing acceptance test snapshot to include new VSA_GENERATED result field.

Changes:
- Modify extractLocalPath() to return error instead of defaulting to
  /tmp/vsa when no local@ path is found, preventing silent data loss
- Update acceptance test snapshot to include VSA_GENERATED result field
  added by the VSA format feature
- Update auto-generated documentation for verify-conforma-konflux-ta task

The security concern about arbitrary file writes is a false positive - the
CLI runs with user permissions and the user explicitly controls the
destination path, similar to mkdir or cp commands.

Assisted-by: Claude Code (Sonnet 4.5)
Rename the two new attestation parameters introduced in this PR to use
generic naming that supports future verification attestation types (e.g.,
Simple Verification Result/SVR) without breaking changes.

Changes:
- Rename --vsa-format to --attestation-format
- Rename --vsa-output-dir to --attestation-output-dir
- Move format/signing-key validation from RunE to PreRunE per review feedback
- Add comprehensive test coverage for path validation and error handling
- Fix Writer to handle both temp directory prefixes and absolute paths
- Add path traversal protection (restrict output to /tmp or workspace)

These flags were introduced in this PR and have not been released, making
this rename non-breaking. Existing VSA flags (--vsa, --vsa-signing-key,
--vsa-upload, --vsa-expiration) remain unchanged.

Security: Validates output paths to prevent writing to arbitrary filesystem
locations. While this is a CLI tool running with user permissions, the
validation provides defense-in-depth for CI/CD environments.

Addresses PR feedback from Joe Stuart on PreRunE validation pattern.

Assisted-by: Claude Code (Sonnet 4.5)
Tests were still using the old flag name --vsa-output-dir instead of
--attestation-output-dir, causing CI failures.

Assisted-by: Claude Code (Sonnet 4.5)
@arewm arewm force-pushed the feature/vsa-format-flag branch from b20c120 to f29560c Compare January 20, 2026 22:32
@joejstuart
Copy link
Contributor

/ok-to-test

@joejstuart joejstuart merged commit aece3e5 into conforma:main Jan 22, 2026
13 checks passed
@arewm arewm deleted the feature/vsa-format-flag branch January 22, 2026 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants