Use successful deployment tags for DAR versions#290
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 16 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (9)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e6b8979. Configure here.
| console.log(` SHA256: ${hash}`); | ||
| console.log(` Size: ${stats.size} bytes`); | ||
| console.log(`${plan.replace ? '✅ Backed up' : '✅ Verified'}: ${key}`); | ||
| console.log(` SHA256: ${sha256}`); |
There was a problem hiding this comment.
Backup skips missing file check
Medium Severity
When planCandidateBackup returns replace: false, backup-dar prints Verified and saves dars.lock without confirming the backup file exists on disk or matches the lock hash. A lock entry with a missing or corrupt file is no longer detected or restored.
Reviewed by Cursor Bugbot for commit e6b8979. Configure here.
There was a problem hiding this comment.
Pull request overview
This PR updates the DAR release/deployment pipeline to treat annotated Git success tags (dar-deploy/<network>/<package>/v<version>) as the source of truth for deployed DAR bytes, and adds a policy layer that enforces immutable deployed artifacts while allowing only the expected mutable candidate slot to change.
Changes:
- Add a DAR version policy implementation + tests to derive the mutable candidate from the latest DevNet success tag (with legacy
dars.locknetwork markers only as fallback). - Require uploads to use the committed backup and prove the fresh build is byte-identical; stop mutating
dars.lockat upload time. - Update CI and release workflows to fetch tags, run the new policy checks, gate deployments, and record success tags only after full provider success.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| scripts/upload-dar.ts | Enforces “fresh build must match committed backup” before uploading; removes lock/network mutation on upload. |
| scripts/dar-version-policy.ts | Implements tag-based deployment evidence, candidate selection, history retention, and deployment gating logic. |
| scripts/dar-version-policy.test.ts | Adds focused unit tests for the new policy logic (candidate selection, immutability, gating, retention). |
| scripts/dar-utils.ts | Removes auto-backup detection and network upload recording helpers (shifts responsibility to tag-based flow). |
| scripts/check-dar-version-policy.ts | Adds a CI/PR check tool to validate backups, fresh builds, history retention, and tag-derived policy. |
| scripts/backup-dar.ts | Refactors backup behavior to replace only the expected candidate while preserving deployed DAR history. |
| package.json | Adds scripts for policy tests and policy checks used by CI/release. |
| .github/workflows/release.yml | Serializes releases, fetches tags, gates DevNet/Mainnet uploads, records annotated success tags, prevents DAR mutation during release finalization, supports idempotent reruns. |
| .github/workflows/ci.yml | Fetches full history + tags, runs policy tests, and enforces base-relative policy checks against origin/main. |
| if (plan.replace) { | ||
| fs.mkdirSync(path.dirname(destination), { recursive: true }); | ||
| const temporary = `${destination}.tmp-${process.pid}`; | ||
| try { | ||
| fs.copyFileSync(source, temporary); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6b89790c6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| run: | | ||
| tag="dar-deploy/mainnet/${PACKAGE_NAME}/v${PACKAGE_VERSION}" | ||
| sha=$(node -e 'const l=require("./dars/dars.lock"); console.log(l.packages[`${process.env.PACKAGE_NAME}/${process.env.PACKAGE_VERSION}/${process.env.PACKAGE_NAME}.dar`].sha256)') | ||
| git tag -a "$tag" -m "Deployed ${PACKAGE_NAME} v${PACKAGE_VERSION} to Mainnet (${sha})" |
There was a problem hiding this comment.
Configure Git before mainnet-only tag creation
When rerunning after DevNet succeeded and pushed dar-deploy/devnet/... but Mainnet did not get tagged, steps.devnet_gate.outputs.tag_exists is true, so the DevNet record step (the only place that runs git config user.name/user.email) is skipped. The Mainnet record step still creates an annotated tag with git tag -a (confirmed via git tag -h: -a creates an annotated tag), and in a clean runner without a configured identity this fails with Git's unable to auto-detect email address, blocking recovery from a partial release. Configure Git in this step too, or before both tag-creation steps.
Useful? React with 👍 / 👎.
| PACKAGE_VERSION: ${{ steps.release_tag.outputs.version }} | ||
| run: | | ||
| tag="dar-deploy/devnet/${PACKAGE_NAME}/v${PACKAGE_VERSION}" | ||
| sha=$(node -e 'const l=require("./dars/dars.lock"); console.log(l.packages[`${process.env.PACKAGE_NAME}/${process.env.PACKAGE_VERSION}/${process.env.PACKAGE_NAME}.dar`].sha256)') |
There was a problem hiding this comment.
Read dars.lock as JSON before tagging deployments
When the deployment tag is missing, this command runs after the upload succeeds, but require("./dars/dars.lock") is not parsed as JSON because .lock is not a registered Node JSON extension; locally, node -e 'require("./dars/dars.lock")' fails with Unexpected token ':'. That means both fresh DevNet and Mainnet releases upload the DAR and then fail before recording the success tag, so reruns cannot become idempotent until this reads the file with fs.readFileSync/JSON.parse or a .json path.
Useful? React with 👍 / 👎.
| const key = getDarLockKey(pkg.name, version, pkg.darName); | ||
| const destination = path.join(getDarsDir(), key); | ||
|
|
||
| if (plan.replace) { |
There was a problem hiding this comment.
Restore the backup file when the lock entry matches
If a current candidate already has a matching dars.lock entry but the DAR file was deleted or corrupted, plan.replace is false and this branch skips both copying the fresh build and verifying destination, then reports ✅ Verified. That breaks the documented recovery path for the current backup is missing / dars.lock lists ... but the file is missing failures, because rerunning npm run backup-dar will not recreate the file until the lock hash or size changes.
Useful? React with 👍 / 👎.
| const plan = planCandidateBackup(pkg, lock, readDeploymentTags(pkg, root), sha256, size); | ||
| const key = getDarLockKey(pkg.name, version, pkg.darName); | ||
| const destination = path.join(getDarsDir(), key); | ||
|
|
||
| if (plan.replace) { | ||
| fs.mkdirSync(path.dirname(destination), { recursive: true }); | ||
| const temporary = `${destination}.tmp-${process.pid}`; | ||
| try { | ||
| fs.copyFileSync(source, temporary); | ||
| if (computeSha256(temporary) !== sha256) throw new Error('Temporary DAR copy failed integrity verification'); | ||
| fs.renameSync(temporary, destination); | ||
| } finally { | ||
| if (fs.existsSync(temporary)) fs.unlinkSync(temporary); | ||
| } | ||
|
|
||
| // Verify hash | ||
| const hash = computeSha256(destPath); | ||
| if (hash !== lock.packages[lockKey].sha256) { | ||
| console.error(`❌ Hash mismatch! File may have been modified.`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // Add network if specified | ||
| if (network && !lock.packages[lockKey].networks.includes(network)) { | ||
| lock.packages[lockKey].networks.push(network); | ||
| lock.packages[lockKey].networks.sort(); | ||
| saveDarsLock(lock); | ||
| console.log(`✅ Added network: ${network}`); | ||
| } | ||
| return; | ||
| lock.packages[key] = { | ||
| sha256, | ||
| size, | ||
| sdkVersion: sdkVersion(pkg.sourceDir), | ||
| uploadedAt: new Date().toISOString(), | ||
| networks: [], | ||
| }; | ||
| } | ||
|
|
||
| // Create backup | ||
| fs.mkdirSync(destDir, { recursive: true }); | ||
| fs.copyFileSync(sourcePath, destPath); | ||
|
|
||
| const hash = computeSha256(destPath); | ||
| const stats = fs.statSync(destPath); | ||
|
|
||
| lock.packages[lockKey] = { | ||
| sha256: hash, | ||
| size: stats.size, | ||
| sdkVersion: getSdkVersion(pkg.sourceDir), | ||
| uploadedAt: new Date().toISOString(), | ||
| networks: network ? [network] : [], | ||
| }; | ||
|
|
||
| // Sort for consistent ordering | ||
| const sorted: Record<string, DarsLockEntry> = {}; | ||
| Object.keys(lock.packages) | ||
| .sort() | ||
| .forEach((k) => { | ||
| sorted[k] = lock.packages[k]; | ||
| }); | ||
| lock.packages = sorted; | ||
|
|
||
| lock.packages = Object.fromEntries( | ||
| Object.entries(lock.packages).sort(([left], [right]) => left.localeCompare(right)) | ||
| ); | ||
| saveDarsLock(lock); | ||
|
|
||
| console.log(`✅ Backed up: ${lockKey}`); | ||
| console.log(` SHA256: ${hash}`); | ||
| console.log(` Size: ${stats.size} bytes`); | ||
| console.log(`${plan.replace ? '✅ Backed up' : '✅ Verified'}: ${key}`); | ||
| console.log(` SHA256: ${sha256}`); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6b89790c6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| run: npm run build | ||
|
|
||
| - name: Check DAR version policy | ||
| run: npm run check:dar-version-policy -- --base origin/main |
There was a problem hiding this comment.
Compare main pushes against the previous commit
When this workflow runs for a push to main, the preceding fetch step updates refs/remotes/origin/main to the commit that is currently being built (git fetch accepts the explicit main:refs/remotes/origin/main refspec). Passing that same ref as --base makes changedPackages() use HEAD as the merge base and see an empty diff, so direct main pushes can skip the DAR version policy entirely. Use github.event.before for main pushes or run the policy with --all in that context.
Useful? React with 👍 / 👎.
| const mode = parseMode(); | ||
| const lock = loadDarsLock(); | ||
| const packages = mode.all ? getAllPackages() : changedPackages(mode.base!); | ||
| const baseLock = mode.base ? readLockAt(mode.base) : undefined; |
There was a problem hiding this comment.
Use the merge base for retention checks
changedPackages() diffs from the merge base, but this loads the retention baseline from the tip of --base. On a feature branch that was cut before main added a DAR backup, CI compares the branch's lock file against the newer origin/main lock and assertHistoryRetention() reports that main's new historical row was deleted even though the branch never touched it. Use the same merge-base commit for baseLock that is used for the path diff.
Useful? React with 👍 / 👎.
| npm run build | ||
| npm run backup-dar -- --package "${{ steps.release_tag.outputs.package_key }}" --version "${{ steps.release_tag.outputs.version }}" | ||
| npm run test:dar-version-policy | ||
| npm run check:dar-version-policy -- --all |
There was a problem hiding this comment.
Avoid running the full migration audit during release
In the committed migration state, OpenCapTable-v34/daml.yaml is 0.0.3 while dars/dars.lock only marks 0.0.1 as deployed unless a dar-deploy/devnet/.../v0.0.2 tag already exists. Running --all here invokes the policy before any upload and requires the candidate to be 0.0.2, so a normal 0.0.3 release is blocked in that state. Gate only the release package against the release tag/tags, or require the migration tag before enabling this release step.
Useful? React with 👍 / 👎.
| const entry = getLockEntry(lock, key); | ||
| const backup = path.join(getDarsDir(), key); | ||
| const built = path.join(ROOT, pkg.sourceDir, '.daml', 'dist', `${pkg.darName}-${pkg.version}.dar`); | ||
| if (!entry || !fs.existsSync(backup)) throw new Error(`${pkg.name}: current backup is missing (${key})`); |
There was a problem hiding this comment.
Verify historical DAR files, not just the current backup
When a branch deletes a non-current historical DAR file but leaves its dars.lock entry intact, changedPackages() selects the package, yet this check only verifies the current version's backup path and assertHistoryRetention() compares lock entries rather than files. That lets CI pass while removing a deployed artifact needed for audit or re-upload; check every retained lock entry's file/hash here or run verify-dars in the CI path.
Useful? React with 👍 / 👎.


Summary
dar-deploy/<network>/<package>/v<version>Scope
CI reads Git history, tags, and committed files only. It has no Canton query, deployment secret,
pull_request_target, or PR-writing path. This PR intentionally does not normalizeOpenCapTable-v34or touch feature/test code. The audit correctly reports that legacy DevNet0.0.1makes0.0.2the candidate while currentmainstill contains noisy0.0.3; stack normalization is separate.Validation
git diff --checkDurable backup and release guidance is updated in the repository wiki.
Note
High Risk
Changes production release and Canton DAR deployment ordering (DevNet tags before Mainnet, immutable deployed artifacts); mistakes could block releases or upload wrong bytes, though policy tests and CI gates mitigate this.
Overview
Replaces live-network /
dars.locknetwork markers as the source of truth with annotated git tagsdar-deploy/<network>/<package>/v<version>, whosedars.lockat the tag defines deployed bytes. A new DAR version policy (scripts + tests) picks the mutable candidate as one patch above the latest DevNet success tag (legacy locknetworksonly as migration fallback), enforces immutable deployed versions, history retention on PRs, and Mainnet upload only after matching DevNet.CI fetches tags and runs
test:dar-version-policypluscheck:dar-version-policy(baseorigin/mainon PR builds); auto-fix pushes are limited to branch refs.Release serializes on
package-release-ocp, gates DevNet/Mainnet upload withcheck:dar-deployment, records success tags after full provider upload, drops release-timebackup-dar, blocks mutatingdars/during artifact commit, and allows idempotent reruns when npm is already published but both deployment tags exist.backup-darandupload-darno longer auto-backup or write networks to the lock; upload requires backup bytes to match a fresh build.Reviewed by Cursor Bugbot for commit e6b8979. Bugbot is set up for automated code reviews on this repo. Configure here.