Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
184 lines
6.0 KiB
TypeScript
184 lines
6.0 KiB
TypeScript
import assert from 'assert'
|
|
import { writeFileSync } from 'fs'
|
|
import * as diff from 'diff'
|
|
import kleur from 'kleur'
|
|
import * as tar from 'tar'
|
|
import tmp from 'tmp'
|
|
import { exec } from './exec'
|
|
import { nicelog } from './nicelog'
|
|
import { PackageDetails, getAllPackageDetails } from './publishing'
|
|
|
|
async function getPackageFirstDiff(
|
|
pkg: PackageDetails,
|
|
publishedVersion: string
|
|
): Promise<Diff | null> {
|
|
assert(process.env.TLDRAW_BEMO_URL, 'TLDRAW_BEMO_URL env var must be set')
|
|
const dir = tmp.dirSync({ unsafeCleanup: true })
|
|
const dirPath = dir.name
|
|
try {
|
|
const unscopedName = pkg.name.replace('@tldraw/', '')
|
|
const url = `https://registry.npmjs.org/${pkg.name}/-/${unscopedName}-${publishedVersion}.tgz`
|
|
const res = await fetch(url)
|
|
if (res.status === 404) {
|
|
// The package is not published at this version. This happens when a
|
|
// new package was added to the monorepo since the last release, or
|
|
// when a previous publish was interrupted after the version-bump
|
|
// commit landed but before npm publish completed. In either case
|
|
// treat it as an added package so the release proceeds instead of
|
|
// failing the entire workflow.
|
|
nicelog(
|
|
`No published tarball for ${pkg.name}@${publishedVersion} (${url}); treating as added.`
|
|
)
|
|
return { type: 'added', packageName: pkg.name, filePath: pkg.name }
|
|
}
|
|
if (res.status >= 400) {
|
|
throw new Error(`Failed to fetch ${url}: ${res.status}`)
|
|
}
|
|
const publishedTarballPath = `${dirPath}/published-package.tgz`
|
|
writeFileSync(publishedTarballPath, Buffer.from(await res.arrayBuffer()))
|
|
const publishedManifest = getTarballManifestSync(publishedTarballPath)
|
|
|
|
const localTarballPath = `${dirPath}/local-package.tgz`
|
|
await exec('yarn', ['pack', '--out', localTarballPath], { pwd: pkg.dir, env: process.env })
|
|
|
|
const localManifest = getTarballManifestSync(localTarballPath)
|
|
|
|
return getManifestFirstDiff(pkg.name, publishedManifest, localManifest)
|
|
} finally {
|
|
dir.removeCallback()
|
|
}
|
|
}
|
|
|
|
type Diff =
|
|
| {
|
|
type: 'added' | 'removed'
|
|
packageName: string
|
|
filePath: string
|
|
}
|
|
| {
|
|
type: 'changed'
|
|
packageName: string
|
|
filePath: string
|
|
diff: string
|
|
}
|
|
|
|
// In the `tldraw` package these files are generated during publishing from the
|
|
// docs content in `apps/docs/content` (see `generate-tldraw-package-docs.ts`),
|
|
// not from SDK source. A docs-only change therefore alters them, which would
|
|
// otherwise make this diff check report a package change and cut a spurious SDK
|
|
// patch release. Ignore them for the `tldraw` package so that docs-only patches
|
|
// don't trigger a new SDK version. Other packages ship a hand-authored DOCS.md
|
|
// as real source, so we only skip these for `tldraw`.
|
|
//
|
|
// `generate-tldraw-package-docs.ts` writes these files to the package root, so
|
|
// in the tarball they live at exactly `package/<name>` (tarball entry paths are
|
|
// prefixed with `package/`). Match the full path rather than the basename so a
|
|
// same-named file in some other folder still counts as a real change.
|
|
const GENERATED_DOCS_FILES = new Set(['package/DOCS.md', 'package/RELEASE_NOTES.md'])
|
|
const GENERATED_DOCS_PACKAGE = 'tldraw'
|
|
|
|
function isGeneratedDocsFile(packageName: string, filePath: string) {
|
|
if (packageName !== GENERATED_DOCS_PACKAGE) return false
|
|
return GENERATED_DOCS_FILES.has(filePath)
|
|
}
|
|
|
|
function getManifestFirstDiff(
|
|
packageName: string,
|
|
a: Record<string, Buffer>,
|
|
b: Record<string, Buffer>
|
|
): Diff | null {
|
|
const aKeys = Object.keys(a).filter((key) => !isGeneratedDocsFile(packageName, key))
|
|
const bKeys = Object.keys(b).filter((key) => !isGeneratedDocsFile(packageName, key))
|
|
for (const key of aKeys) {
|
|
if (!bKeys.includes(key)) {
|
|
return { type: 'removed', packageName, filePath: key }
|
|
}
|
|
if (!a[key].equals(b[key])) {
|
|
return {
|
|
type: 'changed',
|
|
packageName,
|
|
filePath: key,
|
|
diff: diff.createTwoFilesPatch(key, key, a[key].toString(), b[key].toString()),
|
|
}
|
|
}
|
|
}
|
|
for (const key of bKeys) {
|
|
if (!aKeys.includes(key)) {
|
|
return { type: 'added', packageName, filePath: key }
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
function getTarballManifestSync(tarballPath: string) {
|
|
const manifest: Record<string, Buffer> = {}
|
|
tar.list({
|
|
file: tarballPath,
|
|
onentry: (entry) => {
|
|
entry.on('data', (data) => {
|
|
// we could hash these to reduce memory but it's probably fine
|
|
const existing = manifest[entry.path]
|
|
if (existing) {
|
|
manifest[entry.path] = Buffer.concat([existing, data])
|
|
} else {
|
|
manifest[entry.path] = data
|
|
}
|
|
})
|
|
},
|
|
sync: true,
|
|
})
|
|
|
|
return manifest
|
|
}
|
|
|
|
export function formatDiff(diff: Diff) {
|
|
let message = kleur.cyan().bold(diff.packageName) + ' changed:'
|
|
switch (diff.type) {
|
|
case 'added':
|
|
message += `\n New file added ${kleur.green().bold(diff.filePath)}`
|
|
break
|
|
case 'removed':
|
|
message += `\n File removed ${kleur.red().bold(diff.filePath)}`
|
|
break
|
|
case 'changed':
|
|
message += `\n File changed ${kleur.yellow().bold(diff.filePath)}\n\n`
|
|
message += diff.diff
|
|
.split('\n')
|
|
.map((line) => {
|
|
if (line.startsWith('+')) {
|
|
return kleur.green(line)
|
|
} else if (line.startsWith('-')) {
|
|
return kleur.red(line)
|
|
} else {
|
|
return line
|
|
}
|
|
})
|
|
.join('\n')
|
|
|
|
break
|
|
}
|
|
return message
|
|
}
|
|
|
|
/**
|
|
* Compare each local package against the published tarball at
|
|
* `publishedVersion` on npm and return the first detected difference, or null
|
|
* if every package is identical to its published counterpart.
|
|
*
|
|
* Callers should pass the latest version that's actually published on npm
|
|
* (e.g. from `npm show <pkg> version`) rather than the version in the local
|
|
* `package.json`. The local version can drift ahead of npm if a previous
|
|
* publish was interrupted after the version-bump commit was pushed; using the
|
|
* known-published version keeps the diff check robust to that state.
|
|
*/
|
|
export async function getAnyPackageDiff(publishedVersion: string): Promise<Diff | null> {
|
|
const details = await getAllPackageDetails()
|
|
for (const pkg of Object.values(details)) {
|
|
const diff = await getPackageFirstDiff(pkg, publishedVersion)
|
|
if (diff) {
|
|
return diff
|
|
}
|
|
}
|
|
return null
|
|
}
|