Skip to content

feat(provider): add SoftFail mode for downstream-gated verification#580

Open
stan-is-hate wants to merge 1 commit into
pact-foundation:masterfrom
stan-is-hate:feat/soft-fail-mode
Open

feat(provider): add SoftFail mode for downstream-gated verification#580
stan-is-hate wants to merge 1 commit into
pact-foundation:masterfrom
stan-is-hate:feat/soft-fail-mode

Conversation

@stan-is-hate

Copy link
Copy Markdown
Contributor

Summary

Add VerifyRequest.SoftFail to let callers render verification mismatches as a SKIPped subtest instead of a failed one when a downstream gate (e.g. a Pact Broker can-i-merge / can-i-deploy check) owns the authoritative compatibility decision.

Motivation

Callers wrapping pact-go in a larger contract-testing system commonly want this contract:

Mismatches should be recorded to the broker (so the broker's matrix gates can act on them downstream) but should NOT fail the local go test step, because the downstream gates are authoritative and a local fail would either duplicate that signal or contradict it during coordinated rollouts.

Today, the only way to achieve this is at the shell layer (turning off set -o pipefail around go test), which silences everything, including:

  • Verifier panics
  • Broker connection failures
  • Auth errors
  • Configuration errors

That makes "tests didn't actually run but CI is green" a common silent failure mode. We want suppression to be scoped to verification mismatches only — infrastructure failures should still fail loudly.

This PR provides that scope via a per-request flag.

API

A single new bool field on VerifyRequest:

type VerifyRequest struct {
    // ... existing fields ...
    FailIfNoPactsFound         bool
    PublishVerificationResults bool
    SoftFail                   bool  // ← new
}

SoftFail sits next to the other per-request behavior toggles. Default false preserves existing behavior — no caller currently using pact-go sees any change.

Behavior matrix

SoftFail Verifier result Subtest outcome
false (default) nil PASS
false ErrVerifierFailed (mismatch) FAIL
false ErrVerifierFailedToRun (infra) FAIL
true nil PASS
true ErrVerifierFailed (mismatch) SKIP with the error in the skip message
true ErrVerifierFailedToRun (infra) FAIL
true anything else (panic-wrapped, joined, unknown) FAIL

Discrimination uses errors.Is(err, native.ErrVerifierFailed), leveraging the mutual-exclusivity invariant of the two verifier sentinels documented in #579.

Implementation

Two-file change. provider/verify_request.go gets the new field with a thorough doc comment. provider/verifier.go switches the existing if err != nil { t.Error(err) } body of the subtest to a 3-way switch:

t.Run("Provider pact verification", func(t *testing.T) {
    switch {
    case err == nil:
        // PASS — verification succeeded.
    case request.SoftFail && errors.Is(err, native.ErrVerifierFailed):
        t.Skipf("pact verification failed (soft-fail enabled, broker has the record): %v", err)
    default:
        t.Error(err)
    }
})

Why SKIP (not PASS) for the soft-fail mismatch case

Go's stdlib testing package has three outcome states: PASS, FAIL, SKIP. Once a subtest calls t.Error/t.Fatal, the failure propagates to every ancestor and cannot be cleared — c.failed = true is set on the parent chain in Fail() (Go stdlib source, search for func (c *common) Fail()).

That leaves three options for "verification ran, found mismatches, results published, caller deferred to downstream gate":

  • PASS + t.Logf: dishonest — the test report claims verification passed, but verification actually reported mismatches.
  • FAIL: defeats the entire feature — the test fails, propagating to the parent.
  • SKIP with the error in the message: the least-wrong stdlib state. JUnit XML serializes as <skipped message="..."/>, which test dashboards already recognize as "look here for context." Maps cleanly to pytest's xfail semantic.

Backwards compatibility

Pure addition. Zero behavior change for any existing caller (SoftFail defaults to false, which is the existing branch).

Test plan

  • go build ./provider/... clean
  • go vet ./provider/... clean (one pre-existing IPv6 warning unrelated to this PR)
  • go test ./provider/ -short clean
  • CI green

Downstream

Confluent's service-runtime-go library will adopt this immediately once the release ships, replacing its current shell-layer workaround with a single SoftFail: true setting on VerifyRequest. Tracking PR in confluentinc/service-runtime-go#1099 (the strict-mode env var) — this pact-go change is what makes that work correctly under all conditions.

🤖 Generated with Claude Code

Callers that have a downstream authoritative gate (e.g. a Pact Broker
can-i-merge / can-i-deploy check) want verification mismatches to be
recorded to the broker and reported in the test framework, but NOT to
fail CI locally — the downstream gate makes the real decision.

Previously the only way to suppress local failure was at the shell
layer (turning off pipefail around `go test`), which also silenced
verifier panics, broker auth failures, and any other infrastructure
problem. That made "tests didn't actually run but CI is green" a
common failure mode.

New behavior — `VerifyRequest.SoftFail` (default false, preserves
existing behavior):

  - SoftFail = false (default):
      err == nil           → subtest PASS
      err != nil           → subtest FAIL (t.Error)
  - SoftFail = true:
      err == nil           → subtest PASS
      ErrVerifierFailed    → subtest SKIP (t.Skipf, message preserved)
      any other err        → subtest FAIL (t.Error)

Infrastructure errors (ErrVerifierFailedToRun, panics, broker auth)
always fail the subtest, regardless of SoftFail — they signal the
verifier could not produce a result for the downstream gate to act on.

The discrimination uses `errors.Is(err, native.ErrVerifierFailed)`,
which leverages the mutual-exclusivity invariant of the two verifier
sentinels documented in pact-foundation#579.
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 28.288% (-0.04%) from 28.328% — stan-is-hate:feat/soft-fail-mode into pact-foundation:master

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants