Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2a91508
feat(ci): add contract ABI compatibility checks
Eikix Mar 24, 2026
8d439f4
fix(ci): improve abi compat script output
Eikix Mar 24, 2026
cc9151a
fix(contracts): allow accepted ABI deltas in CI
Eikix Mar 25, 2026
a93cd58
fix(contracts): fail local abi wrapper on setup errors
Eikix Mar 25, 2026
b15b826
fix(contracts): document workflow permissions
Eikix Mar 25, 2026
5724ee3
fix(contracts): remove abi check note from readme
Eikix Mar 25, 2026
b1b3bc0
docs(common): add ci readme for abi checks
Eikix Mar 25, 2026
2a572fe
fix(contracts): explain workflow default permissions
Eikix Mar 25, 2026
7277dec
refactor(contracts): group abi compat scripts under ci
Eikix Mar 25, 2026
68f26ca
refactor(contracts): remove old abi compat script paths
Eikix Mar 25, 2026
589c648
fix(contracts): update upgrade check abi helper path
Eikix Mar 25, 2026
fc6f9b2
fix(contracts): correct nested abi compat repo root
Eikix Mar 25, 2026
2ff2496
fix(ci): correct broken import in abi-compat exceptions
Eikix Mar 26, 2026
891232f
refactor(ci): move merge-address-constants to ci/shared
Eikix Mar 26, 2026
52dd342
refactor(ci): group upgrade-check scripts under ci/upgrade-check
Eikix Mar 26, 2026
30f5ac1
fix(ci): harden upgrade-check list.ts error handling
Eikix Mar 26, 2026
5f15262
refactor(ci): extract upgrade-check config to match abi-compat pattern
Eikix Mar 26, 2026
20a2638
docs(ci): update readme with upgrade-check and shared entries
Eikix Mar 26, 2026
9b50494
fix(ci): use inline permission comments for zizmor compliance
Eikix Mar 26, 2026
4450335
fix(ci): remove --force from forge inspect and fix minor code smells
Eikix Mar 26, 2026
59fa81d
fix(ci): restore --force on forge inspect to avoid stale cache
Eikix Mar 26, 2026
de06470
fix(ci): extract JSON from forge stdout to handle compilation preamble
Eikix Mar 26, 2026
4ccd701
fix(common): include deployed ABI compat contracts
Eikix Mar 26, 2026
589a600
chore(common): retrigger ci
Eikix Mar 26, 2026
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
109 changes: 109 additions & 0 deletions .github/workflows/contracts-abi-compat-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: contracts-abi-compat-check

# Default to no token permissions at the workflow level; each job opts into the
# minimum read scopes it needs.
permissions: {}

on:
pull_request:

env:
ABI_COMPAT_FROM_TAG: v0.11.1

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
check-changes:
name: contracts-abi-compat-check/check-changes
permissions:
contents: read # Required by actions/checkout to clone the repository
pull-requests: read # Required by dorny/paths-filter to inspect changed files
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: "false"
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36
id: filter
with:
filters: |
host-contracts:
- .github/workflows/contracts-abi-compat-check.yml
- ci/abi-compat/**
- ci/shared/**
- host-contracts/**
gateway-contracts:
- .github/workflows/contracts-abi-compat-check.yml
- ci/abi-compat/**
- ci/shared/**
- gateway-contracts/**

check:
name: contracts-abi-compat-check/${{ matrix.package }}
needs: check-changes
if: ${{ needs.check-changes.outputs.packages != '[]' }}
permissions:
contents: read # Required by actions/checkout to clone the PR branch and baseline tag
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package: ${{ fromJSON(needs.check-changes.outputs.packages) }}
include:
- package: host-contracts
extra-deps: forge soldeer install
- package: gateway-contracts
extra-deps: ""
steps:
- name: Checkout PR branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: "false"

- name: Checkout baseline
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
ref: ${{ env.ABI_COMPAT_FROM_TAG }}
path: baseline
persist-credentials: "false"

- name: Install Bun
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de

- name: Install PR dependencies
working-directory: ${{ matrix.package }}
run: npm ci

- name: Install baseline dependencies
working-directory: baseline/${{ matrix.package }}
run: npm ci

- name: Install Forge dependencies
if: matrix.extra-deps != ''
env:
PACKAGE: ${{ matrix.package }}
EXTRA_DEPS: ${{ matrix.extra-deps }}
run: |
(cd "$PACKAGE" && $EXTRA_DEPS)
(cd "baseline/$PACKAGE" && $EXTRA_DEPS)

- name: Setup compilation
env:
PACKAGE: ${{ matrix.package }}
run: |
(cd "$PACKAGE" && make ensure-addresses)
(cd "baseline/$PACKAGE" && make ensure-addresses)
bun ci/shared/merge-address-constants.ts "baseline/$PACKAGE/addresses" "$PACKAGE/addresses"
cp "$PACKAGE/foundry.toml" "baseline/$PACKAGE/foundry.toml"

- name: Run ABI compatibility check
env:
PACKAGE: ${{ matrix.package }}
run: bun ci/abi-compat/check.ts "baseline/$PACKAGE" "$PACKAGE" "$PACKAGE"
14 changes: 6 additions & 8 deletions .github/workflows/contracts-upgrade-version-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,13 @@ jobs:
filters: |
host-contracts:
- .github/workflows/contracts-upgrade-version-check.yml
- ci/check-upgrade-versions.ts
- ci/merge-address-constants.ts
- ci/upgrade-version-check-lib.ts
- ci/upgrade-check/**
- ci/shared/**
- host-contracts/**
gateway-contracts:
- .github/workflows/contracts-upgrade-version-check.yml
- ci/check-upgrade-versions.ts
- ci/merge-address-constants.ts
- ci/upgrade-version-check-lib.ts
- ci/upgrade-check/**
- ci/shared/**
- gateway-contracts/**

check:
Expand Down Expand Up @@ -110,11 +108,11 @@ jobs:
# the full union of constants with consistent values (PR wins for shared).
(cd "$PACKAGE" && make ensure-addresses)
(cd "baseline/$PACKAGE" && make ensure-addresses)
bun ci/merge-address-constants.ts "baseline/$PACKAGE/addresses" "$PACKAGE/addresses"
bun ci/shared/merge-address-constants.ts "baseline/$PACKAGE/addresses" "$PACKAGE/addresses"
# Use PR's foundry.toml for both so compiler settings match (cbor_metadata, bytecode_hash)
cp "$PACKAGE/foundry.toml" "baseline/$PACKAGE/foundry.toml"

- name: Run upgrade version check
env:
PACKAGE: ${{ matrix.package }}
run: bun ci/check-upgrade-versions.ts "baseline/$PACKAGE" "$PACKAGE"
run: bun ci/upgrade-check/check.ts "baseline/$PACKAGE" "$PACKAGE"
5 changes: 5 additions & 0 deletions ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CI Scripts

- [`abi-compat/`](./abi-compat/README.md): contract ABI compatibility checks
- [`upgrade-check/`](./upgrade-check/): contract upgrade version checks
- [`shared/`](./shared/): utilities shared across CI subsystems
20 changes: 20 additions & 0 deletions ci/abi-compat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ABI Compatibility

Compare the stable contract ABI surface between two refs locally with:

```bash
bun ci/abi-compat/list.ts --from v0.11.1 --to v0.12.0-0
```

Limit the scope with:

```bash
bun ci/abi-compat/list.ts --from v0.11.1 --to v0.12.0-0 --package host-contracts
bun ci/abi-compat/list.ts --from v0.11.1 --to v0.12.0-0 --package gateway-contracts
```

Use the lower-level checker when both package directories are already prepared:

```bash
bun ci/abi-compat/check.ts <baseline-pkg-dir> <target-pkg-dir> <host-contracts|gateway-contracts>
```
70 changes: 70 additions & 0 deletions ci/abi-compat/check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bun
import { PACKAGE_CONFIG, type PackageName } from "./config";
import { collectAbiCompatResults } from "./lib";

const [baselineDir, targetDir, pkg] = process.argv.slice(2) as [
string | undefined,
string | undefined,
PackageName | undefined,
];

if (!baselineDir || !targetDir || !pkg || !(pkg in PACKAGE_CONFIG)) {
console.error(
"Usage: bun ci/abi-compat/check.ts <baseline-pkg-dir> <target-pkg-dir> <host-contracts|gateway-contracts>",
);
process.exit(1);
}

const results = collectAbiCompatResults(baselineDir, targetDir, pkg);
let errors = 0;

for (const result of results) {
console.log(`::group::Checking ${result.name}`);
try {
if (!result.baselineExists) {
console.log(`Skipping ${result.name} (new contract, not in baseline)`);
continue;
}

console.log(
`${result.name}: ${result.baselineStableCount} stable baseline ABI entries, ${result.targetStableCount} stable target ABI entries`,
);

for (const error of result.errors) {
console.error(`::error::${error}`);
errors++;
}

for (const signature of result.missing) {
console.error(`::error::${result.name} missing stable ABI signature: ${signature}`);
errors++;
}

if (result.allowedMissing.length > 0) {
console.log(`${result.name}: accepted stable ABI exceptions`);
for (const signature of result.allowedMissing) {
console.log(` ~ ${signature}`);
}
}

if (result.added.length > 0) {
console.log(`${result.name}: added stable ABI entries`);
for (const signature of result.added) {
console.log(` + ${signature}`);
}
}

if (result.errors.length === 0 && result.missing.length === 0 && result.added.length === 0) {
console.log(`${result.name}: no stable ABI changes`);
}
} finally {
console.log("::endgroup::");
}
}

if (errors > 0) {
console.error(`::error::ABI compatibility check failed with ${errors} error(s)`);
process.exit(1);
}

console.log("All contracts passed ABI compatibility checks");
44 changes: 44 additions & 0 deletions ci/abi-compat/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export type PackageName = "host-contracts" | "gateway-contracts";

export const EXCLUDED_MODIFIERS = new Set([
"onlyOwner",
"onlyACLOwner",
"onlyGatewayOwner",
"onlyFromEmptyProxy",
"onlyPauser",
"onlyCoprocessorTxSender",
"onlyKmsTxSender",
"onlyRegisteredHostChain",
"onlyHandleFromRegisteredHostChain",
"onlyDecryptionContract",
"onlyInputVerificationContract",
]);

export const EXCLUDED_FUNCTION_PATTERNS = [
/^initialize/,
/^reinitializeV\d+$/,
/^acceptOwnership$/,
/^owner$/,
/^transferOwnership$/,
/^upgradeToAndCall$/,
];

export const EXCLUDED_CONTRACT_FUNCTION_PATTERNS: Record<string, RegExp[]> = {
HCULimit: [/^checkHCUFor/],
};

export const PACKAGE_CONFIG: Record<
PackageName,
{
contracts: string[];
extraDeps?: string;
}
> = {
"host-contracts": {
contracts: ["ACL", "FHEVMExecutor", "HCULimit", "InputVerifier", "KMSVerifier"],
extraDeps: "forge soldeer install",
},
"gateway-contracts": {
contracts: ["CiphertextCommits", "Decryption", "GatewayConfig", "InputVerification", "KMSGeneration"],
},
};
22 changes: 22 additions & 0 deletions ci/abi-compat/exceptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { PackageName } from "./config";

export const ABI_COMPAT_EXCEPTIONS: Partial<Record<PackageName, Partial<Record<string, string[]>>>> = {
"host-contracts": {
ACL: ["error ExpirationDateBeforeOneHour()"],
KMSVerifier: ["event NewContextSet(address[],uint256)"],
},
"gateway-contracts": {
Decryption: [
"error AccountNotAllowedToUseCiphertext(bytes32,address)",
"error PublicDecryptNotAllowed(bytes32)",
"error UserDecryptionNotDelegated(uint256,address,address,address)",
"function isDelegatedUserDecryptionReady((address,address),(bytes32,address)[],bytes) returns (bool)",
],
GatewayConfig: [
"event InitializeGatewayConfig((string,string),(uint256,uint256,uint256,uint256,uint256),(address,address,string,string)[],(address,address,string)[],(address,address,bytes)[])",
"event UpdateKmsNodes((address,address,string,string)[],uint256,uint256,uint256,uint256)",
"function getPublicDecryptionThreshold() returns (uint256)",
"function getUserDecryptionThreshold() returns (uint256)",
],
},
};
Loading
Loading