Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,17 @@ jobs:
run: npm run build

- name: Check DAR version policy
run: npm run check:dar-version-policy -- --base origin/main
env:
BEFORE_SHA: ${{ github.event.before }}
run: |
base=origin/main
if [ "$GITHUB_REF" = "refs/heads/main" ]; then
base="$BEFORE_SHA"
fi
npm run check:dar-version-policy -- --base "$base"

- name: Verify DAR backups
run: npm run verify-dars

- name: Lint DAML
run: npm run lint:daml
Expand Down
19 changes: 10 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,16 @@ jobs:
exit 1
fi

- name: Configure Git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Build and validate package
run: |
npm run build
npm run test:dar-version-policy
npm run check:dar-version-policy -- --all
npm run check:dar-version-policy
npm run lint
npm run format
npm run lint:daml
Expand All @@ -158,7 +163,7 @@ jobs:

- name: Check DevNet deployment
id: devnet_gate
run: npm run check:dar-deployment -- --package "${{ steps.release_tag.outputs.package_key }}" --network devnet
run: npm run check:dar-deployment -- --network devnet

- name: Upload DAR to devnet
if: steps.devnet_gate.outputs.tag_exists != 'true'
Expand All @@ -171,15 +176,13 @@ jobs:
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)')
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
sha=$(node -e 'const fs=require("fs"); const l=JSON.parse(fs.readFileSync("dars/dars.lock", "utf8")); 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 DevNet (${sha})"
git push --no-verify origin "refs/tags/$tag"

- name: Check Mainnet deployment
id: mainnet_gate
run: npm run check:dar-deployment -- --package "${{ steps.release_tag.outputs.package_key }}" --network mainnet
run: npm run check:dar-deployment -- --network mainnet

- name: Upload DAR to mainnet
if: steps.mainnet_gate.outputs.tag_exists != 'true'
Expand All @@ -192,7 +195,7 @@ jobs:
PACKAGE_VERSION: ${{ steps.release_tag.outputs.version }}
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)')
sha=$(node -e 'const fs=require("fs"); const l=JSON.parse(fs.readFileSync("dars/dars.lock", "utf8")); 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})"
git push --no-verify origin "refs/tags/$tag"

Expand Down Expand Up @@ -237,8 +240,6 @@ jobs:
exit 1
fi

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if ! git diff --quiet -- dars || ! git diff --cached --quiet -- dars; then
echo "::error::Release mutated committed DAR backups; refusing to continue."
exit 1
Expand Down
51 changes: 34 additions & 17 deletions scripts/backup-dar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as path from 'path';
import * as yaml from 'yaml';

import { computeSha256, getDarLockKey, getDarsDir, loadDarsLock, saveDarsLock } from './dar-utils';
import { planCandidateBackup, readDeploymentTags } from './dar-version-policy';
import { planCandidateBackup, readDeploymentState } from './dar-version-policy';
import { getAllPackages, getPackage, parsePackageArg, parseVersionArg } from './packages';

function usage(message?: string): never {
Expand All @@ -27,6 +27,29 @@ function sdkVersion(sourceDir: string): string {
return parsed['sdk-version'] ?? 'unknown';
}

/** Restore a missing/corrupt backup atomically, then verify both its hash and size. */
export function ensureBackupFile(source: string, destination: string, sha256: string, size: number): boolean {
const valid =
fs.existsSync(destination) && fs.statSync(destination).size === size && computeSha256(destination) === sha256;
if (!valid) {
fs.mkdirSync(path.dirname(destination), { recursive: true });
const temporary = `${destination}.tmp-${process.pid}`;
try {
fs.copyFileSync(source, temporary);
if (fs.statSync(temporary).size !== size || computeSha256(temporary) !== sha256) {
throw new Error('Temporary DAR copy failed integrity verification');
}
fs.renameSync(temporary, destination);
} finally {
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
}
}
if (fs.statSync(destination).size !== size || computeSha256(destination) !== sha256) {
throw new Error('Backed-up DAR failed integrity verification');
}
return !valid;
}

function main(): void {
const packageArg = parsePackageArg();
const version = parseVersionArg();
Expand All @@ -42,20 +65,12 @@ function main(): void {
const sha256 = computeSha256(source);
const { size } = fs.statSync(source);
const lock = loadDarsLock();
const plan = planCandidateBackup(pkg, lock, readDeploymentTags(pkg, root), sha256, size);
const plan = planCandidateBackup(lock, readDeploymentState(root), sha256, size);
const key = getDarLockKey(pkg.name, version, pkg.darName);
const destination = path.join(getDarsDir(), key);
const restored = ensureBackupFile(source, destination, sha256, size);

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);
}
lock.packages[key] = {
sha256,
size,
Expand All @@ -69,13 +84,15 @@ function main(): void {
Object.entries(lock.packages).sort(([left], [right]) => left.localeCompare(right))
);
saveDarsLock(lock);
console.log(`${plan.replace ? '✅ Backed up' : '✅ Verified'}: ${key}`);
console.log(`${plan.replace || restored ? '✅ Backed up' : '✅ Verified'}: ${key}`);
console.log(` SHA256: ${sha256}`);
}

try {
main();
} catch (error) {
console.error(`❌ ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
if (require.main === module) {
try {
main();
} catch (error) {
console.error(`❌ ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
76 changes: 35 additions & 41 deletions scripts/check-dar-version-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,67 @@ import * as fs from 'fs';
import * as path from 'path';

import { computeSha256, getDarLockKey, getDarsDir, loadDarsLock, type DarsLock } from './dar-utils';
import { assertHistoryRetention, assertPackagePolicy, getLockEntry, readDeploymentTags } from './dar-version-policy';
import { getAllPackages, type PackageConfig } from './packages';
import { assertHistoryRetention, assertPackagePolicy, getLockEntry, readDeploymentState } from './dar-version-policy';
import { requirePackageConfig } from './packages';

const ROOT = path.join(__dirname, '..');
const OCP_SHARED_INPUTS = ['scripts/codegen/', 'libs/splice'];
const pkg = requirePackageConfig('ocp');

function git(args: string[]): string {
return execFileSync('git', args, { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
}

function parseMode(): { all: boolean; base?: string } {
function parseBase(): string | null {
const args = process.argv.slice(2);
const all = args.includes('--all');
const baseIndex = args.indexOf('--base');
const base = baseIndex >= 0 ? args[baseIndex + 1] : undefined;
if (all === Boolean(base) || (baseIndex >= 0 && !base)) {
throw new Error('Use exactly one of --all or --base <git-ref>');
}
return { all, base };
if (args.length === 0) return null;
if (args.length !== 2 || args[0] !== '--base')
throw new Error('Usage: check-dar-version-policy.ts [--base <git-ref>]');
return args[1];
}

function readLockAt(ref: string): DarsLock {
git(['cat-file', '-e', `${ref}^{commit}`]);
return JSON.parse(git(['show', `${ref}:dars/dars.lock`])) as DarsLock;
}

function changedPackages(base: string): PackageConfig[] {
const mergeBase = git(['merge-base', base, 'HEAD']);
const paths = git(['diff', '--name-only', mergeBase, '--']).split('\n').filter(Boolean);
return getAllPackages().filter(
(pkg) =>
paths.some((file) => file.startsWith(`${pkg.sourceDir}/`) || file.startsWith(`dars/${pkg.name}/`)) ||
paths.includes('dars/dars.lock') ||
paths.some((file) => OCP_SHARED_INPUTS.some((input) => file.startsWith(input)))
function packageInputsChanged(comparisonRef: string): boolean {
const paths = git(['diff', '--name-only', comparisonRef, 'HEAD', '--']).split('\n').filter(Boolean);
return paths.some(
(file) =>
file.startsWith(`${pkg.sourceDir}/`) ||
file.startsWith(`dars/${pkg.name}/`) ||
file === 'dars/dars.lock' ||
file.startsWith('scripts/codegen/') ||
file.startsWith('libs/splice/')
);
}

function checkPackage(pkg: PackageConfig, lock: DarsLock, baseLock?: DarsLock): void {
function main(): void {
const base = parseBase();
const comparisonRef = base ? git(['merge-base', base, 'HEAD']) : null;
if (comparisonRef && !packageInputsChanged(comparisonRef)) {
console.log('✅ No deployable package inputs changed');
return;
}

const lock = loadDarsLock();
const key = getDarLockKey(pkg.name, pkg.version, pkg.darName);
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})`);
const backupHash = computeSha256(backup);
if (backupHash !== entry.sha256) throw new Error(`${pkg.name}: backup does not match dars.lock (${key})`);
if (!fs.existsSync(built)) throw new Error(`${pkg.name}: build is missing (${path.relative(ROOT, built)})`);
if (computeSha256(built) !== entry.sha256)
throw new Error(`${pkg.name}: fresh build does not match committed backup`);
if (!entry || !fs.existsSync(backup)) throw new Error(`Current backup is missing (${key})`);
if (fs.statSync(backup).size !== entry.size || computeSha256(backup) !== entry.sha256) {
throw new Error(`Current backup does not match dars.lock (${key})`);
}
if (!fs.existsSync(built)) throw new Error(`Fresh build is missing (${path.relative(ROOT, built)})`);
if (computeSha256(built) !== entry.sha256) throw new Error('Fresh build does not match committed backup');

const tags = readDeploymentTags(pkg, ROOT);
if (baseLock) assertHistoryRetention(pkg, baseLock, lock, tags);
assertPackagePolicy(pkg, lock, tags, entry.sha256);
const state = readDeploymentState(ROOT);
if (comparisonRef) assertHistoryRetention(readLockAt(comparisonRef), lock, state);
assertPackagePolicy(lock, state, entry.sha256);
console.log(`✅ ${pkg.name} v${pkg.version}`);
}

function main(): void {
const mode = parseMode();
const lock = loadDarsLock();
const packages = mode.all ? getAllPackages() : changedPackages(mode.base!);
const baseLock = mode.base ? readLockAt(mode.base) : undefined;
if (packages.length === 0) {
console.log('✅ No deployable package inputs changed');
return;
}
for (const pkg of packages) checkPackage(pkg, lock, baseLock);
}

try {
main();
} catch (error) {
Expand Down
Loading
Loading