Skip to content
Closed
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
27 changes: 22 additions & 5 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ concurrency:

jobs:
release:
# Publish only from main; skip_git_check is the explicit opt-out.
if: github.ref == 'refs/heads/main' || inputs.skip_git_check
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down Expand Up @@ -70,19 +72,34 @@ jobs:

- run: pnpm i

- run: pnpm run check

- run: pnpm test

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

- run: pnpm exec bumpp -r "$VERSION" --yes
# Commit and tag locally only; nothing is pushed until npm has the release.
- run: pnpm exec bumpp -r "$VERSION" --yes --no-push
if: ${{ !inputs.skip_bump }}

- run: pnpm exec changelogithub --draft
if: ${{ !inputs.skip_changelog }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: pnpm run test:pack

# Fail before publishing if the branch moved and the push would be rejected.
- run: git push --dry-run --atomic --follow-tags
if: ${{ !inputs.skip_bump }}

- run: pnpm --filter='./packages/*' publish -r --provenance --access=public --tag="$TAG" $NO_GIT_CHECKS
env:
TAG: ${{ steps.version.outputs.tag }}
NO_GIT_CHECKS: ${{ inputs.skip_git_check && '--no-git-checks' || '' }}

- run: git push --atomic --follow-tags
if: ${{ !inputs.skip_bump }}

# Runs after the push: changelogithub skips the release unless the tag is on GitHub.
- run: pnpm exec changelogithub --draft
if: ${{ !inputs.skip_changelog }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"fix": "uncheck --fix",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:pack": "node scripts/smoke-pack.ts",
"bench": "vitest bench --run",
"sync:sponsors": "node scripts/sync-sponsors.ts && pnpm run fix '**/README.md'"
},
Expand Down
108 changes: 108 additions & 0 deletions scripts/smoke-pack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Checks the tarballs `pnpm publish` would upload, not the sources the tests
// run against: packs every workspace package, installs the tarballs into a
// scratch project outside the workspace, then imports every `exports` entry
// point and typechecks a consumer that imports each of them.
import { execFileSync } from 'node:child_process'
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import process from 'node:process'

const ROOT_DIR = process.cwd()

interface PackageJson {
dependencies?: Record<string, string>
exports?: Record<string, unknown>
}

function run(command: string, args: string[], cwd: string): void {
execFileSync(command, args, { cwd, stdio: 'inherit' })
}

async function readPackageJson(dir: string): Promise<PackageJson> {
// SAFETY: only reads package.json files written by this script, pnpm or npm.
return JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf-8')) as PackageJson
}

async function main(): Promise<void> {
const scratchDir = await mkdtemp(path.join(tmpdir(), 'openapi-spec-smoke-pack-'))

try {
const packDir = path.join(scratchDir, 'packs')
const consumerDir = path.join(scratchDir, 'consumer')

run('pnpm', ['--filter=./packages/*', 'pack', '--pack-destination', packDir], ROOT_DIR)

const tarballs = (await readdir(packDir))
.filter((file) => file.endsWith('.tgz'))
.map((file) => path.join(packDir, file))

if (tarballs.length === 0) {
throw new Error('pnpm pack produced no tarballs')
}

// Installing every tarball at once lets internal dependencies resolve to
// each other, so nothing is fetched for a version npm does not have yet.
await mkdir(consumerDir)
await writeFile(
path.join(consumerDir, 'package.json'),
`${JSON.stringify({ private: true, type: 'module' })}\n`,
)
run('npm', ['install', '--no-audit', '--no-fund', '--ignore-scripts', ...tarballs], consumerDir)

const specifiers: string[] = []
const { dependencies = {} } = await readPackageJson(consumerDir)

for (const name of Object.keys(dependencies)) {
const { exports = {} } = await readPackageJson(path.join(consumerDir, 'node_modules', name))

for (const subpath of Object.keys(exports)) {
if (!subpath.endsWith('.json') && !subpath.includes('*')) {
specifiers.push(path.posix.join(name, subpath))
}
}
}

await writeFile(
path.join(consumerDir, 'smoke.mjs'),
specifiers.map((specifier) => `import ${JSON.stringify(specifier)}\n`).join(''),
)
await writeFile(
path.join(consumerDir, 'smoke.ts'),
specifiers
.map((specifier, i) => `import type * as entry${i} from ${JSON.stringify(specifier)}\n`)
.join(''),
)
await writeFile(
path.join(consumerDir, 'tsconfig.json'),
`${JSON.stringify({
compilerOptions: {
module: 'nodenext',
moduleResolution: 'nodenext',
target: 'es2022',
strict: true,
noEmit: true,
skipLibCheck: false,
types: [],
},
files: ['smoke.ts'],
})}\n`,
)

run(process.execPath, ['smoke.mjs'], consumerDir)
run('pnpm', ['exec', 'tsc', '-p', path.join(consumerDir, 'tsconfig.json')], ROOT_DIR)

// oxlint-disable-next-line no-console
console.log(`Smoke-tested ${specifiers.length} entry points:\n${specifiers.join('\n')}`)
} finally {
await rm(scratchDir, { recursive: true, force: true })
}
}

try {
await main()
} catch (error) {
// A failed command has already printed its own output.
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
}
Loading