Skip to content

Commit 841191c

Browse files
Merge pull request #258 from smartcontractkit/breaking_changes_test
Added a breaking interface test
2 parents bd1d962 + 8fe9ea0 commit 841191c

16 files changed

Lines changed: 412 additions & 14 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
name: Breaking changes
2+
3+
permissions:
4+
contents: read
5+
6+
concurrency:
7+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
8+
cancel-in-progress: true
9+
10+
on:
11+
pull_request:
12+
branches: [main]
13+
# Re-run when override labels are added or removed
14+
types: [opened, synchronize, reopened, labeled, unlabeled]
15+
16+
jobs:
17+
proto-breaking:
18+
runs-on: ubuntu-latest
19+
env:
20+
SKIP: ${{ contains(github.event.pull_request.labels.*.name, 'breaking-change:proto') || contains(github.event.pull_request.labels.*.name, 'breaking-change:approved') }}
21+
steps:
22+
- name: Skipped — intentional breaking change (label override)
23+
if: env.SKIP == 'true'
24+
run: |
25+
echo "::notice title=Proto breaking check skipped::This PR has label breaking-change:proto or breaking-change:approved. A maintainer acknowledged an intentional proto break. Remove the label to re-enable buf breaking."
26+
27+
- name: Checkout code
28+
if: env.SKIP != 'true'
29+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
30+
with:
31+
fetch-depth: 0
32+
submodules: recursive
33+
token: ${{ secrets.GITHUB_TOKEN }}
34+
35+
- name: Setup Bun
36+
if: env.SKIP != 'true'
37+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
38+
with:
39+
bun-version: 1.3.12
40+
41+
- name: Install dependencies
42+
if: env.SKIP != 'true'
43+
run: bun install --frozen-lockfile
44+
45+
# cre-sdk's buf.yaml points at ../../submodules/chainlink-protos/cre, which buf rejects when
46+
# --against uses subdir=packages/cre-sdk (module path escapes the context). Run breaking on
47+
# the chainlink-protos workspace instead, comparing HEAD to the submodule commit pinned on main.
48+
- name: Buf breaking (proto)
49+
if: env.SKIP != 'true'
50+
run: |
51+
set -euo pipefail
52+
REPO="${{ github.workspace }}"
53+
SUBMODULE="${REPO}/submodules/chainlink-protos"
54+
BASE_COMMIT=$(git -C "$REPO" rev-parse origin/main:submodules/chainlink-protos)
55+
BASE_DIR="${RUNNER_TEMP}/chainlink-protos-baseline"
56+
if ! git -C "$SUBMODULE" cat-file -e "${BASE_COMMIT}^{commit}" 2>/dev/null; then
57+
git -C "$SUBMODULE" fetch --no-tags origin "${BASE_COMMIT}"
58+
fi
59+
git -C "$SUBMODULE" worktree add "${BASE_DIR}" "${BASE_COMMIT}" --detach
60+
cleanup() {
61+
git -C "$SUBMODULE" worktree remove "${BASE_DIR}" --force || true
62+
}
63+
trap cleanup EXIT
64+
(cd "$SUBMODULE" && bun x @bufbuild/buf breaking cre --against "${BASE_DIR}/cre" --error-format github-actions)
65+
66+
ts-api-surface:
67+
runs-on: ubuntu-latest
68+
env:
69+
SKIP: ${{ contains(github.event.pull_request.labels.*.name, 'breaking-change:typescript-api') || contains(github.event.pull_request.labels.*.name, 'breaking-change:approved') }}
70+
steps:
71+
- name: Skipped — intentional breaking change (label override)
72+
if: env.SKIP == 'true'
73+
run: |
74+
echo "::notice title=TypeScript API check skipped::This PR has label breaking-change:typescript-api or breaking-change:approved. Commit an updated api-baseline.d.ts when appropriate; this label only bypasses CI."
75+
76+
- name: Checkout code
77+
if: env.SKIP != 'true'
78+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
79+
with:
80+
submodules: recursive
81+
token: ${{ secrets.GITHUB_TOKEN }}
82+
83+
- name: Setup Bun
84+
if: env.SKIP != 'true'
85+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
86+
with:
87+
bun-version: 1.3.12
88+
89+
- name: Install dependencies
90+
if: env.SKIP != 'true'
91+
run: bun install --frozen-lockfile
92+
93+
- name: Diff TypeScript public API vs baseline
94+
if: env.SKIP != 'true'
95+
working-directory: packages/cre-sdk
96+
run: |
97+
bun run update-api-baseline
98+
99+
if ! git diff --exit-code api-baseline.d.ts; then
100+
echo "::error::TypeScript public API surface changed. Run 'bun run update-api-baseline' locally and commit the updated api-baseline.d.ts."
101+
exit 1
102+
fi
103+
104+
host-bindings:
105+
runs-on: ubuntu-latest
106+
env:
107+
SKIP: ${{ contains(github.event.pull_request.labels.*.name, 'breaking-change:host-bindings') || contains(github.event.pull_request.labels.*.name, 'breaking-change:approved') }}
108+
steps:
109+
- name: Skipped — intentional breaking change (label override)
110+
if: env.SKIP == 'true'
111+
run: |
112+
echo "::notice title=Host bindings check skipped::This PR has label breaking-change:host-bindings or breaking-change:approved. Update snapshots and host-imports-baseline.txt when appropriate; this label only bypasses CI."
113+
114+
- name: Checkout code
115+
if: env.SKIP != 'true'
116+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
117+
with:
118+
submodules: recursive
119+
token: ${{ secrets.GITHUB_TOKEN }}
120+
121+
- name: Setup Bun
122+
if: env.SKIP != 'true'
123+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
124+
with:
125+
bun-version: 1.3.12
126+
127+
- name: Install dependencies
128+
if: env.SKIP != 'true'
129+
run: bun install --frozen-lockfile
130+
131+
- name: JS host bindings snapshot test
132+
if: env.SKIP != 'true'
133+
working-directory: packages/cre-sdk
134+
run: bun test src/sdk/wasm/host-bindings-contract.test.ts
135+
136+
- name: Diff Rust host imports vs baseline
137+
if: env.SKIP != 'true'
138+
run: |
139+
grep -E '^\s+fn [a-z_]+\(' \
140+
packages/cre-sdk-javy-plugin/src/javy_chainlink_sdk/src/lib.rs \
141+
| grep -v '{$' > /tmp/current-imports.txt
142+
143+
if ! diff packages/cre-sdk-javy-plugin/src/javy_chainlink_sdk/host-imports-baseline.txt \
144+
/tmp/current-imports.txt; then
145+
echo "::error::Rust host import signatures changed. Update host-imports-baseline.txt if intentional."
146+
exit 1
147+
fi

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ jobs:
110110
echo ""
111111
echo "=========================================="
112112
echo "Uncommitted file changes detected after full-checks!"
113+
echo "Changed files:"
114+
git diff --name-only
113115
echo "Run 'bun full-checks' locally and commit the resulting changes."
114116
echo "=========================================="
115117
exit 1

.github/workflows/template-compatibility-comment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ jobs:
4040
with:
4141
script: |
4242
const path = require('path');
43-
const run = require(path.join(process.env.GITHUB_WORKSPACE, 'scripts', 'template-compatibility-comment.js'));
43+
const run = require(path.join(process.env.GITHUB_WORKSPACE, 'scripts', 'template-compatibility-comment.cjs'));
4444
await run({ github, context });

PUBLISHING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ There are 2 possible scenarios that you might encounter:
2323

2424
Below ale the steps for two scenarios.
2525

26+
> **Breaking changes:** If the release includes any intentional breaking changes to the
27+
> TypeScript API surface, JS host bindings, or Rust host imports, the corresponding baseline
28+
> files (`api-baseline.d.ts`, `__snapshots__/host-bindings-contract.test.ts.snap`,
29+
> `host-imports-baseline.txt`) must already be committed on the release branch before
30+
> tagging. The `breaking-changes` CI job must be green before publishing.
31+
2632
### 1. Both packages need an update
2733

2834
1. Create a new branch from `main` with the name `release-candidate-vx.y.z` (for example `release-candidate-v1.0.8`).

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,32 @@ bun generate:chain-selectors # Update chain selector types
266266
bun generate:sdk # Generate all SDK types and code
267267
```
268268

269+
### Breaking Changes
270+
271+
The [`breaking-changes`](./.github/workflows/breaking-changes.yml) workflow blocks PRs that alter any of the three protected contracts. If your change is intentional, update the relevant baseline before pushing:
272+
273+
| Contract | What triggers the failure | How to update |
274+
|---|---|---|
275+
| Proto fields | Field deleted, renamed, renumbered, or type changed | No baseline file — CI runs `buf breaking` on `submodules/chainlink-protos` (`cre` module) against the submodule commit pinned on `main` |
276+
| TypeScript public API | An exported type/interface was removed or changed | Run `bun run update-api-baseline` inside `packages/cre-sdk` and commit `api-baseline.d.ts` |
277+
| JS host binding names | A binding was added, removed, or renamed in `host-bindings.ts` | Run `bun test --update-snapshots` inside `packages/cre-sdk` and commit the updated `__snapshots__` file |
278+
| Rust host imports | An `extern "C"` import was added or removed in `lib.rs` | Re-run the sed extraction (see `breaking-changes.yml`) and commit `host-imports-baseline.txt` |
279+
280+
#### CI override labels
281+
282+
When a change is **intentionally** breaking and cannot be fixed by updating a baseline (e.g. a coordinated proto break before `main` catches up), a **maintainer** adds the matching label on the PR (this re-runs the workflow):
283+
284+
| Label | Skips |
285+
|-------|--------|
286+
| `breaking-change:proto` | `proto-breaking` (`buf breaking`) |
287+
| `breaking-change:typescript-api` | `ts-api-surface` |
288+
| `breaking-change:host-bindings` | `host-bindings` |
289+
| `breaking-change:approved` | All three jobs |
290+
291+
Labels are an audit trail in the PR timeline, not a substitute for review. Prefer updating baselines (`api-baseline.d.ts`, snapshots, `host-imports-baseline.txt`) when the new contract is the new source of truth. For proto breaks, coordinate the `chainlink-protos` submodule bump and document migration notes in the PR.
292+
293+
Restrict who can add these labels in the repo’s **Labels** settings (e.g. maintainers only).
294+
269295
For detailed development setup, see individual package READMEs:
270296

271297
- [CRE SDK Development](./packages/cre-sdk/README.md#building-from-source)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
fn call_capability(req_ptr: *const u8, req_len: i32) -> i64;
2+
fn await_capabilities(
3+
fn get_secrets(
4+
fn await_secrets(
5+
fn log(message_ptr: *const u8, message_len: i32);
6+
fn send_response(response_ptr: *const u8, response_len: i32) -> i32;
7+
fn switch_modes(mode: i32);
8+
fn random_seed(mode: i32) -> i64;
9+
fn now(result_timestamp: *mut u8) -> i32;

packages/cre-sdk/api-baseline.d.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
export * from './sdk';
2+
export * as EVM_PB from '@cre/generated/capabilities/blockchain/evm/v1alpha/client_pb';
3+
export * as SOLANA_PB from '@cre/generated/capabilities/blockchain/solana/v1alpha/client_pb';
4+
export * as CONFIDENTIAL_HTTP_CLIENT_PB from '@cre/generated/capabilities/networking/confidentialhttp/v1alpha/client_pb';
5+
export * as HTTP_CLIENT_PB from '@cre/generated/capabilities/networking/http/v1alpha/client_pb';
6+
export * as HTTP_TRIGGER_PB from '@cre/generated/capabilities/networking/http/v1alpha/trigger_pb';
7+
export * as CRON_TRIGGER_PB from '@cre/generated/capabilities/scheduler/cron/v1/trigger_pb';
8+
export * as SDK_PB from '@cre/generated/sdk/v1alpha/sdk_pb';
9+
export * as VALUES_PB from '@cre/generated/values/v1/values_pb';
10+
export * as BUFBUILD_TYPES from '@cre/sdk/types/bufbuild-types';
11+
export * from './cre';
12+
export * from './don-info';
13+
export * from './errors';
14+
export * from './report';
15+
export type * from './runtime';
16+
export * from './runtime';
17+
export * from './types/bufbuild-types';
18+
export * from './utils';
19+
export * from './utils/capabilities/http/http-helpers';
20+
export * from './wasm';
21+
export * from './workflow';
22+
import type { Message } from '@bufbuild/protobuf';
23+
import type { GenMessage } from '@bufbuild/protobuf/codegenv2';
24+
import type { ReportRequest, ReportRequestJson } from '@cre/generated/sdk/v1alpha/sdk_pb';
25+
import type { Report } from '@cre/sdk/report';
26+
import type { ConsensusAggregation, PrimitiveTypes, UnwrapOptions } from '@cre/sdk/utils';
27+
import type { SecretsProvider } from '.';
28+
export type { ReportRequest, ReportRequestJson };
29+
export type CallCapabilityParams<I extends Message, O extends Message> = {
30+
capabilityId: string;
31+
method: string;
32+
payload: I;
33+
inputSchema: GenMessage<I>;
34+
outputSchema: GenMessage<O>;
35+
};
36+
/**
37+
* Base runtime available in both DON and Node execution modes.
38+
* Provides core functionality for calling capabilities and logging.
39+
*/
40+
export interface BaseRuntime<C> {
41+
config: C;
42+
callCapability<I extends Message, O extends Message>(params: CallCapabilityParams<I, O>): {
43+
result: () => O;
44+
};
45+
now(): Date;
46+
log(message: string): void;
47+
}
48+
/**
49+
* Runtime for Node mode execution.
50+
*/
51+
export interface NodeRuntime<C> extends BaseRuntime<C> {
52+
readonly _isNodeRuntime: true;
53+
}
54+
/**
55+
* Runtime for DON mode execution.
56+
*/
57+
export interface Runtime<C> extends BaseRuntime<C>, SecretsProvider {
58+
/**
59+
* Executes a function in Node mode and aggregates results via consensus.
60+
*
61+
* @param fn - Function to execute in each node (receives NodeRuntime)
62+
* @param consensusAggregation - How to aggregate results across nodes
63+
* @param unwrapOptions - Optional unwrapping config for complex return types
64+
* @returns Wrapped function that returns aggregated result
65+
*/
66+
runInNodeMode<TArgs extends unknown[], TOutput>(fn: (nodeRuntime: NodeRuntime<C>, ...args: TArgs) => TOutput, consensusAggregation: ConsensusAggregation<TOutput, true>, unwrapOptions?: TOutput extends PrimitiveTypes ? never : UnwrapOptions<TOutput>): (...args: TArgs) => {
67+
result: () => TOutput;
68+
};
69+
report(input: ReportRequest | ReportRequestJson): {
70+
result: () => Report;
71+
};
72+
}
73+
import type { Message } from '@bufbuild/protobuf';
74+
import type { Secret, SecretRequest, SecretRequestJson } from '@cre/generated/sdk/v1alpha/sdk_pb';
75+
import { type Runtime } from '@cre/sdk/runtime';
76+
import type { Trigger } from '@cre/sdk/utils/triggers/trigger-interface';
77+
import type { CreSerializable } from './utils';
78+
export type HandlerFn<TConfig, TTriggerOutput, TResult> = (runtime: Runtime<TConfig>, triggerOutput: TTriggerOutput) => Promise<CreSerializable<TResult>> | CreSerializable<TResult>;
79+
export interface HandlerEntry<TConfig, TRawTriggerOutput extends Message<string>, TTriggerOutput, TResult> {
80+
trigger: Trigger<TRawTriggerOutput, TTriggerOutput>;
81+
fn: HandlerFn<TConfig, TTriggerOutput, TResult>;
82+
}
83+
export type Workflow<TConfig> = ReadonlyArray<HandlerEntry<TConfig, any, any, any>>;
84+
export declare const handler: <TRawTriggerOutput extends Message<string>, TTriggerOutput, TConfig, TResult>(trigger: Trigger<TRawTriggerOutput, TTriggerOutput>, fn: HandlerFn<TConfig, TTriggerOutput, TResult>) => HandlerEntry<TConfig, TRawTriggerOutput, TTriggerOutput, TResult>;
85+
export type SecretsProvider = {
86+
getSecret(request: SecretRequest | SecretRequestJson): {
87+
result: () => Secret;
88+
};
89+
};
90+
import type { SecretRequest } from '@cre/generated/sdk/v1alpha/sdk_pb';
91+
export declare class DonModeError extends Error {
92+
constructor();
93+
}
94+
export declare class NodeModeError extends Error {
95+
constructor();
96+
}
97+
export declare class SecretsError extends Error {
98+
secretRequest: SecretRequest;
99+
error: string;
100+
constructor(secretRequest: SecretRequest, error: string);
101+
}
102+
export declare class NullReportError extends Error {
103+
constructor();
104+
}
105+
export declare class WrongSignatureCountError extends Error {
106+
constructor();
107+
}
108+
export declare class ParseSignatureError extends Error {
109+
constructor();
110+
}
111+
export declare class RecoverSignerError extends Error {
112+
constructor();
113+
}
114+
export declare class UnknownSignerError extends Error {
115+
constructor();
116+
}
117+
export declare class DuplicateSignerError extends Error {
118+
constructor();
119+
}
120+
export declare class RawReportTooShortError extends Error {
121+
readonly need: number;
122+
readonly got: number;
123+
constructor(need: number, got: number);
124+
}
125+
import { type ReportResponse, type ReportResponseJson } from '@cre/generated/sdk/v1alpha/sdk_pb';
126+
import { type Environment, type Zone } from './don-info';
127+
import type { BaseRuntime } from './runtime';
128+
export type ReportParseConfig = {
129+
acceptedZones?: Zone[];
130+
acceptedEnvironments?: Environment[];
131+
skipSignatureVerification?: boolean;
132+
};
133+
export declare const REPORT_METADATA_HEADER_LENGTH = 109;
134+
export type ReportMetadataHeader = {
135+
version: number;
136+
executionId: string;
137+
timestamp: number;
138+
donId: number;
139+
donConfigVersion: number;
140+
workflowId: string;
141+
workflowName: string;
142+
workflowOwner: string;
143+
reportId: string;
144+
body: Uint8Array;
145+
};
146+
export declare class Report {
147+
private readonly report;
148+
private cachedHeader;
149+
constructor(report: ReportResponse | ReportResponseJson);
150+
static parse(runtime: BaseRuntime<unknown>, rawReport: Uint8Array, signatures: Uint8Array[], reportContext: Uint8Array, config?: ReportParseConfig): Promise<Report>;
151+
private parseHeader;
152+
private verifySignaturesWithConfig;
153+
seqNr(): bigint;
154+
configDigest(): Uint8Array;
155+
reportContext(): Uint8Array;
156+
rawReport(): Uint8Array;
157+
version(): number;
158+
executionId(): string;
159+
timestamp(): number;
160+
donId(): number;
161+
donConfigVersion(): number;
162+
workflowId(): string;
163+
workflowName(): string;
164+
workflowOwner(): string;
165+
reportId(): string;
166+
body(): Uint8Array;
167+
x_generatedCodeOnly_unwrap(): ReportResponse;
168+
}
169+
/**
170+
* Test-only surface for the CRE SDK: test framework and (when generated) capability mocks.
171+
* Use for unit testing runtime logic without the WASM execution environment.
172+
*/
173+
export { type CapabilityHandler, DEFAULT_MAX_RESPONSE_SIZE_BYTES, getTestCapabilityHandler, type NewTestRuntimeOptions, newTestRuntime, REPORT_METADATA_HEADER_LENGTH, RESPONSE_BUFFER_TOO_SMALL, registerTestCapability, type Secrets, TestRuntime, type TestRuntimeState, test, } from '../testutils/test-runtime';
174+
export { TestWriter } from '../testutils/test-writer';
175+
export { type AddContractMockOptions, addContractMock, type ContractMock, type WriteReportMockInput, } from './evm-contract-mock';
176+
export * from './generated';

0 commit comments

Comments
 (0)