verify.ts 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. /**
  2. * Verify a release family's version baseline, and — when publishing — that the
  3. * run comes from the family's tag and its members are publishable.
  4. *
  5. * Publication happens only from GitHub Actions, so the tag and publishability
  6. * checks are gates on the workflow, not advisory local warnings
  7. * ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
  8. */
  9. import { parseArgs } from 'node:util'
  10. import { isEntry } from './process.ts'
  11. import { releaseFamily, type ReleaseFamily, type ReleaseMember } from './families.ts'
  12. /**
  13. * Assert every member may be published: npm refuses a `private` package.
  14. * @param members - the family's members.
  15. */
  16. function verifyPublishable(members: readonly ReleaseMember[]): void {
  17. const priv = members.filter(member => member.manifest.private === true)
  18. if (priv.length > 0) {
  19. throw new Error(`publishing requires removing "private": true from:\n${priv.map(member => member.directory).join('\n')}`)
  20. }
  21. }
  22. /**
  23. * Assert the workflow runs from a tag this family publishes from, and that the
  24. * tag names a version the family actually carries.
  25. * @param family - the release family.
  26. * @param members - the family's members.
  27. * @param ref - the `GITHUB_REF` value.
  28. */
  29. function verifyTag(family: ReleaseFamily, members: readonly ReleaseMember[], ref: string): void {
  30. const prefix = 'refs/tags/'
  31. if (!ref.startsWith(prefix)) {
  32. throw new Error(`publishing release family ${family.id} requires running from a ${family.tagPrefix}* tag, got ${ref || '(no ref)'}`)
  33. }
  34. const tag = ref.slice(prefix.length)
  35. if (!tag.startsWith(family.tagPrefix)) {
  36. throw new Error(`tag ${tag} does not belong to release family ${family.id} (expected ${family.tagPrefix}*)`)
  37. }
  38. const expected = members.map(member => family.tagFor(member))
  39. if (!expected.includes(tag)) {
  40. throw new Error(`tag ${tag} names no version this family carries; its members would tag as:\n${[...new Set(expected)].join('\n')}`)
  41. }
  42. }
  43. /** Run the verification for the family named by `--family`. */
  44. function main(): void {
  45. const { values } = parseArgs({
  46. options: { family: { type: 'string' } },
  47. allowPositionals: false,
  48. })
  49. if (values.family === undefined) throw new Error('usage: verify.ts --family <dsh|vendor>')
  50. const family = releaseFamily(values.family)
  51. const members = family.members(process.cwd())
  52. family.verifyVersions(members)
  53. // Resolve the publish order here, before the build: an install-edge cycle
  54. // makes the order unrepresentable, and that has to surface at the first gate
  55. // rather than when pack is already writing tarballs.
  56. const ordered = family.publishOrder(members)
  57. if (ordered.length !== members.length) {
  58. throw new Error(
  59. `release family ${family.id}: publish order covers ${String(ordered.length)} of ${String(members.length)} members`,
  60. )
  61. }
  62. const publishing = process.env.RELEASE_PUBLISH === 'true'
  63. if (publishing) {
  64. verifyPublishable(members)
  65. verifyTag(family, members, process.env.GITHUB_REF ?? '')
  66. }
  67. const versions = [...new Set(members.map(member => member.version))]
  68. const summary = versions.length === 1 ? versions[0] : `${String(versions.length)} versions`
  69. console.log(`release verify: family ${family.id}, ${String(members.length)} member(s), ${summary}, publish order resolved${publishing ? ', publish gates passed' : ''}`)
  70. }
  71. if (isEntry(import.meta.url)) main()