diff --git a/.github/workflows/interface-sync.yml b/.github/workflows/interface-sync.yml new file mode 100644 index 00000000..d074cec0 --- /dev/null +++ b/.github/workflows/interface-sync.yml @@ -0,0 +1,52 @@ +name: Interface Sync Guard + +permissions: {} + +# Guards the cross-language interface contract: +# +# The frontend SDK bundles generated Candid declarations (idlFactory + types) +# that MUST stay in sync with the canister .did files. This job regenerates the +# declarations from the committed .did using the pinned bindgen version and +# fails if the result differs from what is checked in — i.e. someone changed a +# .did but forgot to regenerate the frontend declarations (or edited the +# generated files by hand). +# +# Note: the Rust <-> Motoko backend interface contract is already enforced +# behaviorally — the Motoko canister is compiled to wasm and the Rust canister's +# integration tests run against it (see backend/mo/canisters/*/Makefile and +# backend-motoko.yml), so this guard deliberately focuses on the +# frontend <-> canister seam, which is otherwise unguarded. + +on: + push: + branches: + - main + - master + pull_request: + paths: + - backend/rs/canisters/*/*.did + - frontend/ic_vetkeys/src/declarations/** + - frontend/ic_vetkeys/make_did_bindings.sh + - .github/workflows/interface-sync.yml + +jobs: + declarations-in-sync: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + - name: Regenerate frontend declarations from committed .did files + working-directory: frontend/ic_vetkeys + run: SKIP_EXTRACT_CANDID=1 bash make_did_bindings.sh + - name: Fail if declarations are out of sync + run: | + set -eEuo pipefail + if ! git diff --exit-code -- frontend/ic_vetkeys/src/declarations; then + echo "::error::Frontend declarations are out of sync with the canister .did files." + echo "Run 'bash make_did_bindings.sh' in frontend/ic_vetkeys and commit the result." + exit 1 + fi diff --git a/frontend/ic_vetkeys/make_did_bindings.sh b/frontend/ic_vetkeys/make_did_bindings.sh index efe3acbb..41d97b3b 100755 --- a/frontend/ic_vetkeys/make_did_bindings.sh +++ b/frontend/ic_vetkeys/make_did_bindings.sh @@ -1,17 +1,34 @@ set -ex +# Pin the bindgen version so regeneration is reproducible. The committed +# declarations under src/declarations were generated with this version; the +# interface-sync CI guard regenerates and diffs against them, so an unpinned +# version (whatever `npx` resolves to) would cause spurious failures whenever a +# newer bindgen ships cosmetic/formatting changes. Bump this deliberately and +# commit the regenerated output together. +BINDGEN_VERSION="0.3.0" + +# Set SKIP_EXTRACT_CANDID=1 to regenerate the TS declarations from the existing +# committed .did files without rebuilding them from the Rust source. The +# interface-sync CI guard uses this so the check stays reproducible (it depends +# only on the pinned bindgen version and the committed .did, not on the +# candid-extractor / wasm toolchain). +SKIP_EXTRACT_CANDID="${SKIP_EXTRACT_CANDID:-0}" + function make_and_copy_declarations () { DIR=$1 NAME=$2 DID_FILE=$3 - pushd "$DIR/$NAME" - make extract-candid - popd + if [ "$SKIP_EXTRACT_CANDID" != "1" ]; then + pushd "$DIR/$NAME" + make extract-candid + popd + fi rm -rf "src/declarations/$NAME" mkdir -p "src/declarations/$NAME" - npx @icp-sdk/bindgen --did-file "$DIR/$NAME/$DID_FILE" --out-dir "src/declarations/$NAME" --declarations-flat --force + npx "@icp-sdk/bindgen@${BINDGEN_VERSION}" --did-file "$DIR/$NAME/$DID_FILE" --out-dir "src/declarations/$NAME" --declarations-flat --force } make_and_copy_declarations "../../backend/rs/canisters/" "ic_vetkeys_manager_canister" "ic_vetkeys_manager_canister.did"