-
Notifications
You must be signed in to change notification settings - Fork 0
Use successful deployment tags for DAR versions #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ on: | |
| type: string | ||
|
|
||
| concurrency: | ||
| group: package-release-${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }} | ||
| group: package-release-ocp | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
|
|
@@ -48,6 +48,9 @@ jobs: | |
| git lfs install | ||
| git lfs pull | ||
|
|
||
| - name: Fetch deployment tags | ||
| run: git fetch --tags origin | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | ||
| with: | ||
|
|
@@ -114,12 +117,21 @@ jobs: | |
| run: scripts/install-dpm-sdks.sh | ||
|
|
||
| - name: Validate npm package version | ||
| id: npm_version | ||
| run: | | ||
| PACKAGE_NAME=$(node -p "require('./package.json').name") | ||
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | ||
| if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then | ||
| echo "::error::${PACKAGE_NAME}@${PACKAGE_VERSION} is already published. Bump package.json before creating a package release tag." | ||
| exit 1 | ||
| devnet_tag="dar-deploy/devnet/${{ steps.release_tag.outputs.package }}/v${{ steps.release_tag.outputs.version }}" | ||
| mainnet_tag="dar-deploy/mainnet/${{ steps.release_tag.outputs.package }}/v${{ steps.release_tag.outputs.version }}" | ||
| if ! git show-ref --verify --quiet "refs/tags/${devnet_tag}" || ! git show-ref --verify --quiet "refs/tags/${mainnet_tag}"; then | ||
| echo "::error::${PACKAGE_NAME}@${PACKAGE_VERSION} is already published without both deployment success tags. Refusing a new release over an existing npm version." | ||
| exit 1 | ||
| fi | ||
| echo "${PACKAGE_NAME}@${PACKAGE_VERSION} and both deployment tags already exist; resuming post-publish finalization." | ||
| echo "already_published=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "already_published=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Validate release tag is current main | ||
|
|
@@ -134,7 +146,8 @@ jobs: | |
| - name: Build and validate package | ||
| run: | | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the committed migration state, Useful? React with 👍 / 👎. |
||
| npm run lint | ||
| npm run format | ||
| npm run lint:daml | ||
|
|
@@ -143,12 +156,46 @@ jobs: | |
| npm run verify-package | ||
| npm run test | ||
|
|
||
| - name: Check DevNet deployment | ||
| id: devnet_gate | ||
| run: npm run check:dar-deployment -- --package "${{ steps.release_tag.outputs.package_key }}" --network devnet | ||
|
|
||
| - name: Upload DAR to devnet | ||
| if: steps.devnet_gate.outputs.tag_exists != 'true' | ||
| run: npm run upload-dar -- --package "${{ steps.release_tag.outputs.package_key }}" --network devnet | ||
|
|
||
| - name: Record DevNet deployment tag | ||
| if: steps.devnet_gate.outputs.tag_exists != 'true' | ||
| env: | ||
| PACKAGE_NAME: ${{ steps.release_tag.outputs.package }} | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the deployment tag is missing, this command runs after the upload succeeds, but Useful? React with 👍 / 👎. |
||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| 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 | ||
|
|
||
| - name: Upload DAR to mainnet | ||
| if: steps.mainnet_gate.outputs.tag_exists != 'true' | ||
| run: npm run upload-dar -- --package "${{ steps.release_tag.outputs.package_key }}" --network mainnet | ||
|
|
||
| - name: Record Mainnet deployment tag | ||
| if: steps.mainnet_gate.outputs.tag_exists != 'true' | ||
| env: | ||
| PACKAGE_NAME: ${{ steps.release_tag.outputs.package }} | ||
| 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)') | ||
| git tag -a "$tag" -m "Deployed ${PACKAGE_NAME} v${PACKAGE_VERSION} to Mainnet (${sha})" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When rerunning after DevNet succeeded and pushed Useful? React with 👍 / 👎. |
||
| git push --no-verify origin "refs/tags/$tag" | ||
|
|
||
| - name: Detect factory state | ||
| id: factory | ||
| run: | | ||
|
|
@@ -176,6 +223,7 @@ jobs: | |
| run: npm run package:manifest | ||
|
|
||
| - name: Publish npm package | ||
| if: steps.npm_version.outputs.already_published != 'true' | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| run: npm publish | ||
|
|
@@ -191,8 +239,11 @@ jobs: | |
|
|
||
| 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 | ||
| fi | ||
| for path in \ | ||
| dars \ | ||
| generated/ocp-factory-contract-id.json \ | ||
| generated/ocp-factory-contract-id.json.d.ts \ | ||
| generated/npm-manifest.txt \ | ||
|
|
@@ -210,4 +261,4 @@ jobs: | |
| fi | ||
|
|
||
| git commit -m "chore: record release artifacts for ${RELEASE_TAG} [skip ci]" | ||
| git push origin HEAD:main | ||
| git push --no-verify origin HEAD:main | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,131 +1,81 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * Backup a DAR file after mainnet upload. Copies from .daml/dist/ to dars/{package}/{version}/ and updates dars.lock. | ||
| * | ||
| * **Retention:** When bumping a package version, add the new backup with this script but **do not remove** prior | ||
| * `dars/<package>/<oldVersion>/` trees that are already in the repo—keep historical DARs for audit, re-upload, and | ||
| * debugging. See the repo wiki: https://github.com/Fairmint/open-captable-protocol-daml/wiki | ||
| * | ||
| * Usage: tsx scripts/backup-dar.ts --package <name> --version <version> [--network <network>] | ||
| */ | ||
| /** Replace the one undeployed candidate backup while preserving every deployed DAR. */ | ||
|
|
||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import * as yaml from 'yaml'; | ||
| import { computeSha256, getDarsDir, loadDarsLock, saveDarsLock, type DarsLockEntry } from './dar-utils'; | ||
| import { getAllPackages, getPackage, parseNetworkArg, parsePackageArg, parseVersionArg } from './packages'; | ||
|
|
||
| function printUsage(errorMessage?: string): never { | ||
| if (errorMessage) console.error(`❌ ${errorMessage}\n`); | ||
| console.error('Usage: tsx scripts/backup-dar.ts --package <name> --version <version> [--network <network>]'); | ||
| console.error('\nPackages:'); | ||
| for (const pkg of getAllPackages()) { | ||
| console.error(` ${pkg.name}`); | ||
| } | ||
| import { computeSha256, getDarLockKey, getDarsDir, loadDarsLock, saveDarsLock } from './dar-utils'; | ||
| import { planCandidateBackup, readDeploymentTags } from './dar-version-policy'; | ||
| import { getAllPackages, getPackage, parsePackageArg, parseVersionArg } from './packages'; | ||
|
|
||
| function usage(message?: string): never { | ||
| if (message) console.error(`❌ ${message}\n`); | ||
| console.error('Usage: tsx scripts/backup-dar.ts --package <name> --version <version>'); | ||
| console.error( | ||
| `Packages: ${getAllPackages() | ||
| .map((pkg) => pkg.name) | ||
| .join(', ')}` | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| function getSdkVersion(sourceDir: string): string { | ||
| const damlYamlPath = path.join(__dirname, '..', sourceDir, 'daml.yaml'); | ||
| if (!fs.existsSync(damlYamlPath)) return 'unknown'; | ||
|
|
||
| try { | ||
| const content = fs.readFileSync(damlYamlPath, 'utf-8'); | ||
| const parsed = yaml.parse(content); | ||
| return parsed?.['sdk-version'] ?? 'unknown'; | ||
| } catch { | ||
| return 'unknown'; | ||
| } | ||
| function sdkVersion(sourceDir: string): string { | ||
| const parsed = yaml.parse(fs.readFileSync(path.join(__dirname, '..', sourceDir, 'daml.yaml'), 'utf8')) as { | ||
| 'sdk-version'?: string; | ||
| }; | ||
| return parsed['sdk-version'] ?? 'unknown'; | ||
| } | ||
|
|
||
| function main() { | ||
| function main(): void { | ||
| const packageArg = parsePackageArg(); | ||
| const version = parseVersionArg(); | ||
| const network = parseNetworkArg(); | ||
|
|
||
| if (!packageArg || !version) printUsage('Missing required arguments'); | ||
|
|
||
| if (!packageArg || !version) usage('Missing required arguments'); | ||
| const pkg = getPackage(packageArg); | ||
| if (!pkg) printUsage(`Unknown package: ${packageArg}`); | ||
| if (!pkg) usage(`Unknown package: ${packageArg}`); | ||
| if (version !== pkg.version) usage(`daml.yaml is v${pkg.version}, not v${version}`); | ||
|
|
||
| const rootDir = path.join(__dirname, '..'); | ||
| const darsDir = getDarsDir(); | ||
|
|
||
| // Build paths | ||
| const sourcePath = path.join(rootDir, pkg.sourceDir, '.daml', 'dist', `${pkg.darName}-${version}.dar`); | ||
| const destDir = path.join(darsDir, pkg.name, version); | ||
| const destPath = path.join(destDir, `${pkg.darName}.dar`); | ||
| const lockKey = `${pkg.name}/${version}/${pkg.darName}.dar`; | ||
|
|
||
| // Check source exists | ||
| if (!fs.existsSync(sourcePath)) { | ||
| console.error(`❌ Source DAR not found: ${sourcePath}`); | ||
| console.error(' Run "npm run build" first'); | ||
| process.exit(1); | ||
| } | ||
| const root = path.join(__dirname, '..'); | ||
| const source = path.join(root, pkg.sourceDir, '.daml', 'dist', `${pkg.darName}-${version}.dar`); | ||
| if (!fs.existsSync(source)) usage(`Source DAR not found: ${source}. Run npm run build first.`); | ||
|
|
||
| const sha256 = computeSha256(source); | ||
| const { size } = fs.statSync(source); | ||
| const lock = loadDarsLock(); | ||
|
|
||
| // Already backed up? | ||
| if (lockKey in lock.packages) { | ||
| console.log(`ℹ️ Already backed up: ${lockKey}`); | ||
|
|
||
| if (!fs.existsSync(destPath)) { | ||
| console.error(`❌ Lock entry exists but file missing: ${destPath}`); | ||
| process.exit(1); | ||
| 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a current candidate already has a matching Useful? React with 👍 / 👎. |
||
| fs.mkdirSync(path.dirname(destination), { recursive: true }); | ||
| const temporary = `${destination}.tmp-${process.pid}`; | ||
| try { | ||
| fs.copyFileSync(source, temporary); | ||
|
Comment on lines
+49
to
+53
|
||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Backup skips missing file checkMedium Severity When Reviewed by Cursor Bugbot for commit e6b8979. Configure here.
Comment on lines
+45
to
+73
|
||
| } | ||
|
|
||
| try { | ||
| main(); | ||
| } catch (err) { | ||
| console.error('❌ Error:', err); | ||
| } catch (error) { | ||
| console.error(`❌ ${error instanceof Error ? error.message : String(error)}`); | ||
| process.exit(1); | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this workflow runs for a push to
main, the preceding fetch step updatesrefs/remotes/origin/mainto the commit that is currently being built (git fetchaccepts the explicitmain:refs/remotes/origin/mainrefspec). Passing that same ref as--basemakeschangedPackages()useHEADas the merge base and see an empty diff, so direct main pushes can skip the DAR version policy entirely. Usegithub.event.beforefor main pushes or run the policy with--allin that context.Useful? React with 👍 / 👎.