cargo build in Release and Deploy Workflows Missing –locked, Allowing Dependency Drift from CI-Validated Lockfile
The release.yml and deploy-testnet.yml GitHub Actions workflows invoke cargo build without the --locked flag, while every Cargo invocation in ci.yml explicitly passes --locked.
At a Glance
The release.yml and deploy-testnet.yml GitHub Actions workflows invoke cargo build without the --locked flag, while every Cargo invocation in ci.yml explicitly passes --locked.
Summary
The release.yml and deploy-testnet.yml GitHub Actions workflows invoke cargo build without the --locked flag, while every Cargo invocation in ci.yml explicitly passes --locked. This allows Cargo to silently resolve a dependency graph that differs from the one committed in Cargo.lock and validated by CI, meaning the WASM binaries shipped to users and recorded by SHA256 checksum in release notes may have never been exercised by the test suite. Build reproducibility is also broken: rebuilding the same Git tag at a later date can produce different checksums.
Root-Cause Analysis
Confirmed evidence:
ci.ymlpasses--lockedon all three of its Cargo invocations (clippy,build,test).release.yml'sBuild Release Contractsstep omits--locked:
run: cargo build --target wasm32v1-none --releasedeploy-testnet.yml'sBuild Contractsstep omits--lockedin the same way.deploy-testnet.ymlalready uses--lockedforcargo install stellar-cli, proving awareness of the flag and making the omission in the project's own build steps an oversight rather than a deliberate policy.- Issue #63 previously introduced
--lockedinto CI to resolve a lockfile pinning incident, but the release path was not updated at the same time.
Confirmed impact:
Without --locked, Cargo consults the network resolver and may select dependency versions that satisfy the version constraints in Cargo.toml but differ from the exact versions pinned in Cargo.lock. This is especially likely after a dependency publishes a patch release between CI and a release build.
Reasonable inference:
The omission is an oversight introduced when the release and deploy workflows were written independently of — or before — issue #63 hardened ci.yml. There is no evidence this was intentional.
Security dimension (label: security):
The published WASM binaries are the authoritative on-chain artifacts. Their SHA256 checksums are the only integrity reference users have. If the resolved dependency set differs from what CI tested, a supply-chain substitution in a transitive dependency could reach production without any test coverage detecting it.
Resolution Steps
- Verify
Cargo.lockis committed and current before applying the flag. Runcargo build --target wasm32v1-none --releaselocally and confirmCargo.lockis unchanged (no diff). Commit any needed updates first, because once--lockedis enforced a stale lockfile will fail the build immediately.
- Add
--lockedtorelease.ymlin theBuild Release Contractsstep.
- Add
--lockedtodeploy-testnet.ymlin theBuild Contractsstep.
- Audit all remaining Cargo invocations across every workflow file to confirm uniform use of
--locked. Pay attention to anycargo generate-lockfile,cargo update, orcargo installsteps for project crates.
- Open a dry-run PR with only the flag additions and verify the release workflow succeeds end-to-end against the current committed lockfile before merging.
- Validate failure behavior by deliberately making
Cargo.lockstale (see CLI Commands) and confirming the build exits non-zero with the expected Cargo error.
CLI Commands
Check for local lockfile drift before enabling --locked:
cargo build --target wasm32v1-none --release
git diff Cargo.lockIf the diff is non-empty, update and commit the lockfile:
git add Cargo.lock
git commit -m "chore: update Cargo.lock before enforcing --locked in release workflow"Simulate a stale lockfile to verify failure behavior:
# Add a harmless version bump to a dependency in Cargo.toml,
# then attempt a --locked build to confirm it fails.
cargo build --locked --target wasm32v1-none --releaseExpected error output when the lockfile is stale:
error: the lock file /path/to/Cargo.lock needs to be updated but --locked was passed to prevent thisAudit all workflow files for Cargo invocations missing --locked:
grep -rn "cargo " .github/workflows/ | grep -v "\-\-locked"Review every line returned; not all Cargo subcommands require --locked (e.g., cargo install for external tools may be intentionally pinned separately), but all project build, test, and lint steps should use it.
Configuration Snippets
release.yml — corrected Build Release Contracts step:
- name: Build Release Contracts
run: cargo build --locked --target wasm32v1-none --releasedeploy-testnet.yml — corrected Build Contracts step:
- name: Build Contracts
run: cargo build --locked --target wasm32v1-none --releaseReference pattern from ci.yml (no changes needed):
run: cargo clippy --locked --all-targets --all-features --target wasm32v1-none -- -D warnings
run: cargo build --locked --target wasm32v1-none --release
run: cargo test --locked --verboseVerification
After applying the fix, confirm the release workflow succeeds:
- Merge the corrected workflow files.
- Trigger the release workflow (either via tag push or manual dispatch).
- Confirm the build step exits with code
0and produces WASM artifacts.
Confirm artifact integrity:
Build the same commit locally with --locked and compare SHA256 checksums against the CI-produced artifacts:
cargo build --locked --target wasm32v1-none --release
sha256sum target/wasm32v1-none/release/<CONTRACT_NAME>.wasmThe digest must match the value recorded in the release notes.
Confirm stale-lockfile failure (regression gate):
In a branch, manually edit one version specifier in Cargo.toml without running cargo update, then trigger the workflow. The build must fail with Cargo's --locked error and must not produce any artifact.
Audit command — expected output after fix:
grep -rn "cargo build\|cargo test\|cargo clippy" .github/workflows/ | grep -v "\-\-locked"This command should return no output after the fix is applied.
Prevention
Enforce lockfile commitment in CI:
Add a step to ci.yml that fails if Cargo.lock would be modified by any build step, catching drift before it reaches the release path:
- name: Verify Cargo.lock is up to date
run: |
cargo update --dry-run 2>&1 | grep -q "Updating" && \
echo "Cargo.lock is stale; run cargo update and commit the result" && exit 1 || trueRepository setting — require Cargo.lock to be committed:
Ensure .gitignore does not exclude Cargo.lock for binary or WASM crates. For library crates this is a matter of policy; for deployable artifacts, committing Cargo.lock is mandatory.
Branch protection / PR check:
Add a required status check that runs git diff --exit-code Cargo.lock after cargo build --locked to prevent merging PRs that leave the lockfile dirty.
Monitoring — release artifact digest tracking:
Record the SHA256 of each published WASM binary in a machine-readable artifact manifest (e.g., a checksums.json committed to the release). A downstream script can re-build from the same tag with --locked and assert digest equality, catching any reproducibility regression automatically.
Periodic cargo audit in CI:
- name: Security audit
run: |
cargo install --locked cargo-audit
cargo audit --lockedThis complements --locked by alerting on known-vulnerable dependency versions pinned in the lockfile, closing the loop between lockfile integrity and supply-chain security.