Skip to content
Open
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
11 changes: 11 additions & 0 deletions .github/test/certified_connectors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema_note": "Connectors in this list are certified against real sandbox APIs on every PR that touches shared/core/proto code, or that touches the connector's own files. Adding a connector here is a reviewed change and requires: (1) live sandbox credentials present in the CI creds file, (2) a working scenario+assert in its suite spec, verified to pass with --skip-dependencies decided deliberately per scenario. Do not add a connector here speculatively. This manifest is the ongoing-regression tier: it re-verifies an already-proven connector on every relevant future change. A connector's own live-sandbox proof lives separately, as a running record, in that connector's own connector_specs/<name>/specs.json's verified_scenarios array (see ci.yml's 'Verify newly added connectors' declared scenarios' step) — it grows across PRs as new payment methods/flows are added, it is not a one-time gate. Promotion into this manifest is a later, deliberate decision after a connector has real verified_scenarios coverage.",
"connectors": [
{
"name": "stripe",
"suite": "PaymentService/Authorize",
"scenario": "no3ds_auto_capture_credit_card",
"skip_dependencies": true
}
]
}
246 changes: 244 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ jobs:
any-rs: ${{ steps.filter.outputs.any-rs }}
ci: ${{ steps.filter.outputs.ci }}
connector-names: ${{ steps.connector-names.outputs.list }}
certified-connectors-touched: ${{ steps.certified-connectors.outputs.touched }}
new-connectors: ${{ steps.new-connectors.outputs.list }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down Expand Up @@ -105,7 +107,8 @@ jobs:
- 'scripts/**'
stripe:
- 'crates/integrations/connector-integration/src/connectors/stripe.rs'
- 'crates/integrations/connector-integration/src/connectors/stripe/transformers.rs'
- 'crates/integrations/connector-integration/src/connectors/stripe/**'
- 'crates/internal/integration-tests/src/connector_specs/stripe/**'
any-rs:
- '**/*.rs'
ci:
Expand All @@ -123,6 +126,56 @@ jobs:
echo "list=$names" >> $GITHUB_OUTPUT
echo "Changed connectors: $names"

- name: Check whether changed connectors are certified
id: certified-connectors
shell: bash
run: |
touched="false"

# A certified connector's own files changed.
IFS=',' read -ra CONNECTORS <<< "${{ steps.connector-names.outputs.list }}"
for c in "${CONNECTORS[@]}"; do
[[ -z "$c" ]] && continue
if jq -e --arg c "$c" '.connectors[] | select(.name == $c)' \
.github/test/certified_connectors.json > /dev/null; then
touched="true"
break
fi
done

# The manifest itself changed (e.g. adding a new certified
# connector) — must also trigger a run, or a newly-manifested
# connector's first certification silently waits for some
# unrelated future PR to touch its own files.
if git diff --name-only "${{ github.event.pull_request.base.sha || github.event.before }}" HEAD \
-- .github/test/certified_connectors.json | grep -q .; then
touched="true"
fi

echo "touched=$touched" >> $GITHUB_OUTPUT
echo "Certified connector touched: $touched"

- name: Detect newly added connectors
id: new-connectors
if: steps.connector-names.outputs.list != ''
shell: bash
run: |
base_ref="${{ github.event.pull_request.base.sha || github.event.before }}"
new_list=""
IFS=',' read -ra CONNECTORS <<< "${{ steps.connector-names.outputs.list }}"
for c in "${CONNECTORS[@]}"; do
spec_dir="crates/internal/integration-tests/src/connector_specs/$c"
# A connector is "new" in this PR if its spec directory does not
# exist at the PR's base commit, regardless of whether it exists
# now. Compares against the merge-base, not just the immediate
# parent, so it's correct for PRs with multiple commits.
if ! git cat-file -e "${base_ref}:${spec_dir}" 2>/dev/null; then
new_list="${new_list}${new_list:+,}${c}"
fi
done
echo "list=$new_list" >> $GITHUB_OUTPUT
echo "Newly added connectors: $new_list"

# ── Fast Checks ───────────────────────────────────────────────────────────

typos:
Expand Down Expand Up @@ -502,7 +555,8 @@ jobs:
echo "CONNECTOR_AUTH_FILE_PATH=${{ github.workspace }}/.github/test/creds.json" >> $GITHUB_ENV
echo "Connector credentials available for tests"
else
echo "No connector credentials available - tests will use default values"
echo "No connector credentials available"
exit 1
fi

- name: Free up disk space
Expand Down Expand Up @@ -561,6 +615,194 @@ jobs:
-E "$SCHEMA_EXCLUDE"
fi

# Certification runs a real gRPC call against each certified connector's
# live sandbox. Scoped ONLY to the certified connectors' own files
# (certified-connectors-touched) — deliberately NOT triggered by
# core/proto/CI changes generally.
#
# Why not core/proto: a full-fleet, all-connector, real-sandbox regression
# run already exists (Jenkins "Generate Test Report" job in
# hyperswitch-infra, connector-service-report-pipeline branch,
# .jenkins/connector-service/generate-test-report.Jenkinsfile) and covers
# the "did this core/proto change break some connector" question at the
# pre-tag/nightly level, where it belongs — that's where deployment gets
# blocked, not individual PRs. Firing this per-PR gate on every core/proto
# change would tax every unrelated PR (e.g. a Citigate PR touching
# router_data.rs for its own field) with a real Stripe API call it has
# nothing to do with — the exact blast-radius problem this scoping avoids.
- name: Determine whether connector certification should run
if: ${{ env.RUN_TESTS == 'true' }}
id: cert-gate
shell: bash
run: |
CERTIFIED_TOUCHED="${{ needs.changes.outputs.certified-connectors-touched }}"

if [[ "${CERTIFIED_TOUCHED}" == "true" ]]; then
echo "run=true" >> $GITHUB_OUTPUT
else
echo "run=false" >> $GITHUB_OUTPUT
echo "Skipping certification — no certified connector's own files changed"
fi

- name: Install grpcurl for test_ucs certification
if: ${{ env.RUN_TESTS == 'true' && (steps.cert-gate.outputs.run == 'true' || needs.changes.outputs.new-connectors != '') }}
shell: bash
run: |
if ! command -v grpcurl >/dev/null 2>&1; then
GRPCURL_VERSION="1.9.3"
curl -fsSL \
"https://github.com/fullstorydev/grpcurl/releases/download/v${GRPCURL_VERSION}/grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz" \
-o /tmp/grpcurl.tar.gz
tar -xzf /tmp/grpcurl.tar.gz -C /tmp grpcurl
sudo mv /tmp/grpcurl /usr/local/bin/grpcurl
fi
grpcurl --version

- name: Run certified connector authorizations via test_ucs
if: ${{ env.RUN_TESTS == 'true' && steps.cert-gate.outputs.run == 'true' }}
shell: bash
run: |
manifest=".github/test/certified_connectors.json"
count=$(jq '.connectors | length' "$manifest")

if [[ "$count" -eq 0 ]]; then
echo "No certified connectors configured — nothing to run"
exit 0
fi

failures=0
for i in $(seq 0 $((count - 1))); do
name=$(jq -r ".connectors[$i].name" "$manifest")
suite=$(jq -r ".connectors[$i].suite" "$manifest")
scenario=$(jq -r ".connectors[$i].scenario" "$manifest")
skip_deps=$(jq -r ".connectors[$i].skip_dependencies // false" "$manifest")

if ! jq -e --arg c "$name" 'has($c)' "${CONNECTOR_AUTH_FILE_PATH}" > /dev/null; then
echo "::error::Certified connector '$name' has no entry in the CI credentials file — cannot certify"
failures=$((failures + 1))
continue
fi

echo "::group::Certifying $name ($suite / $scenario)"
args=(--skip-setup --connector "$name" --suite "$suite" --scenario "$scenario" --interface grpc --report)
[[ "$skip_deps" == "true" ]] && args+=(--skip-dependencies)

# A real transformer/proto regression fails every attempt, so this
# retry only buys back the CI runs a genuine sandbox hiccup (timeout,
# transient 5xx from the connector) would otherwise cost an innocent PR.
attempt=1
max_attempts=3
succeeded="false"
until [[ "$attempt" -gt "$max_attempts" ]]; do
if ./scripts/run-tests "${args[@]}"; then
succeeded="true"
break
fi
echo "::warning::Attempt $attempt/$max_attempts failed for $name ($suite / $scenario)"
attempt=$((attempt + 1))
[[ "$attempt" -le "$max_attempts" ]] && sleep 10
done

if [[ "$succeeded" != "true" ]]; then
echo "::error::Certification failed for $name ($suite / $scenario) after $max_attempts attempts"
failures=$((failures + 1))
fi
echo "::endgroup::"
done

if [[ "$failures" -gt 0 ]]; then
echo "::error::$failures certified connector(s) failed certification"
exit 1
fi

# A brand-new connector (its connector_specs/<name>/ directory did not
# exist at this PR's base commit) gets a live-sandbox proof requirement
# on its own PR, regardless of whether it ever joins
# certified_connectors.json. This is separate from cert-gate above:
# it fires only for connectors newly added in *this* PR, not on every
# core/proto change, and it does not depend on the reviewed manifest.
#
# The connector author declares readiness themselves, in their own
# specs.json (verified_scenarios: an array, since a connector can — and
# for real coverage should — have multiple proven scenarios: different
# payment methods, capture modes, flows, not just one Authorize call).
# Each entry needs { suite, scenario, skip_dependencies, has_live_creds,
# no_creds_reason }. has_live_creds: false is not a silent or unexplained
# pass — no_creds_reason is mandatory in that case, visible directly in
# the PR diff (not just a CI log line), so "merged without live proof,
# and why" stays in the file a reviewer is already looking at. This
# array is expected to grow across later PRs as new payment methods/
# flows are added to the connector — it's a running record, not a
# one-time gate. has_live_creds: true actually runs the call and must
# PASS.
- name: Verify newly added connectors' declared scenarios
if: ${{ env.RUN_TESTS == 'true' && needs.changes.outputs.new-connectors != '' }}
shell: bash
run: |
failures=0
IFS=',' read -ra NEW_CONNECTORS <<< "${{ needs.changes.outputs.new-connectors }}"
for name in "${NEW_CONNECTORS[@]}"; do
specs_path="crates/internal/integration-tests/src/connector_specs/${name}/specs.json"
if [[ ! -f "$specs_path" ]]; then
echo "::error::New connector '$name' has no specs.json at $specs_path"
failures=$((failures + 1))
continue
fi

entry_count=$(jq '.verified_scenarios // [] | length' "$specs_path")
if [[ "$entry_count" -eq 0 ]]; then
echo "::error::New connector '$name' has no \"verified_scenarios\" array in $specs_path — every new connector must declare at least one entry { suite, scenario, has_live_creds, no_creds_reason (if has_live_creds is false) } so its own PR proves (or explicitly and visibly defers) a real sandbox call. See stripe's certified_connectors.json entry for the suite/scenario shape."
failures=$((failures + 1))
continue
fi

for i in $(seq 0 $((entry_count - 1))); do
entry=$(jq -c ".verified_scenarios[$i]" "$specs_path")
has_live_creds=$(jq -r '.has_live_creds // false' <<< "$entry")
suite=$(jq -r '.suite // empty' <<< "$entry")
scenario=$(jq -r '.scenario // empty' <<< "$entry")
skip_deps=$(jq -r '.skip_dependencies // false' <<< "$entry")
no_creds_reason=$(jq -r '.no_creds_reason // empty' <<< "$entry")

if [[ -z "$suite" || -z "$scenario" ]]; then
echo "::error::New connector '$name' verified_scenarios[$i] is missing suite/scenario in $specs_path"
failures=$((failures + 1))
continue
fi

if [[ "$has_live_creds" != "true" ]]; then
if [[ -z "$no_creds_reason" ]]; then
echo "::error::New connector '$name' verified_scenarios[$i] ($suite / $scenario) has has_live_creds: false but no no_creds_reason — a bare false with no explanation is not accepted, say why (e.g. \"sandbox access requested, pending <team/ticket>\")."
failures=$((failures + 1))
continue
fi
echo "::warning::New connector '$name' verified_scenarios[$i] ($suite / $scenario) declares has_live_creds: false — merging WITHOUT live sandbox proof. Reason: $no_creds_reason. Tracked in $specs_path, not silently accepted: this connector cannot be added to certified_connectors.json until re-verified with real creds."
continue
fi

if ! jq -e --arg c "$name" 'has($c)' "${CONNECTOR_AUTH_FILE_PATH}" > /dev/null; then
echo "::error::New connector '$name' verified_scenarios[$i] declares has_live_creds: true but has no entry in the CI credentials file — fix the declaration or add the creds"
failures=$((failures + 1))
continue
fi

echo "::group::Verifying $name ($suite / $scenario)"
args=(--skip-setup --connector "$name" --suite "$suite" --scenario "$scenario" --interface grpc --report)
[[ "$skip_deps" == "true" ]] && args+=(--skip-dependencies)

if ! ./scripts/run-tests "${args[@]}"; then
echo "::error::Verification failed for new connector $name ($suite / $scenario)"
failures=$((failures + 1))
fi
echo "::endgroup::"
done
done

if [[ "$failures" -gt 0 ]]; then
echo "::error::$failures new connector scenario(s) failed verification"
exit 1
fi

# ── SDK Tests (FFI + gRPC) ────────────────────────────────────────────────
# Builds all SDK binaries then runs gRPC tests first, followed by FFI tests
# and mock tests. Running in a single job avoids the external-services feature
Expand Down
Loading
Loading