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
2 changes: 1 addition & 1 deletion .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by scripts/gen-release-config.mjs — do not edit by hand.
# Generated by `pnpm release:config` (cl-release labels) — do not edit by hand.

'pkg:checkout':
- changed-files:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
CL_CLI_CLIENT_SECRET: ${{ secrets.CL_CLI_CLIENT_SECRET }}
# Optional: the checkout and links suites are skipped without it
CL_CLI_SALES_CHANNEL_CLIENT_ID: ${{ secrets.CL_CLI_SALES_CHANNEL_CLIENT_ID }}
run: node scripts/test/live-sweep.mjs
run: node packages/test-utils/bin/live-sweep.mjs

- name: Run integration suites
env: *credentials
Expand All @@ -67,4 +67,4 @@ jobs:
- name: Remove this run's test resources
if: always()
env: *credentials
run: node scripts/test/live-sweep.mjs
run: node packages/test-utils/bin/live-sweep.mjs
10 changes: 5 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Publish

# Publishes to npm when a human publishes a draft release created by
# release.yml, then announces it on Slack. The tagged package is published
# (scripts/publish.mjs), built and tested from exactly the tag, together with
# (`cl-release publish`), built and tested from exactly the tag, together with
# the workspace dependencies it needs that aren't on npm yet (cli-core,
# cli-ux, … released in the same chore(release) PR); their draft releases
# are then marked published too.
Expand Down Expand Up @@ -44,12 +44,12 @@ jobs:
node-version: 'lts/*'
cache: 'pnpm'

# Before resolve-tag, which needs the release scripts' dependencies
# Before resolving the tag: cl-release and its dependencies
- run: pnpm install --frozen-lockfile

- name: Resolve package
id: meta
run: node scripts/resolve-tag.mjs "$TAG" | tee -a "$GITHUB_OUTPUT"
run: pnpm exec cl-release resolve "$TAG" | tee -a "$GITHUB_OUTPUT"

- name: Build the package and its workspace dependencies
run: pnpm --filter '${{ steps.meta.outputs.package }}...' build
Expand All @@ -63,11 +63,11 @@ jobs:
run: pnpm --filter '${{ steps.meta.outputs.package }}...' --workspace-concurrency=1 test

- name: Command surface vs npm
run: node scripts/check-manifest.mjs '${{ steps.meta.outputs.dir }}'
run: pnpm exec cl-release manifest '${{ steps.meta.outputs.dir }}'

- name: Publish
id: publish
run: node scripts/publish.mjs "$TAG"
run: pnpm exec cl-release publish "$TAG"

- name: Publish the draft releases of the dependencies published along
if: steps.publish.outputs.published_deps != ''
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ name: Release
# here: publish.yml publishes once someone publishes the draft.
#
# Tags are `<dir>-v<version>`, where <dir> is the package's directory under
# packages/ or plugins/ (scripts/tag.mjs creates them). The package is derived
# packages/ or plugins/ (`pnpm release:tag` creates them). The package is derived
# from the tag, so adding a package needs no change here. Legacy bare `v*`
# tags don't match `*-v*` and stay inert.
#
# Release notes come from the package's Conventional Commits since its
# previous release (scripts/release-notes.mjs, conventional-changelog): only
# previous release (`cl-release notes`, conventional-changelog): only
# the commits that touched its folder, however long the range.
on:
push:
Expand All @@ -32,17 +32,17 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 'lts/*', cache: 'pnpm' }
# The release scripts' own dependencies only (the root package)
- run: pnpm install --frozen-lockfile --filter @commercelayer/cli-monorepo
# The release tooling only: the root package and cl-release (packages/release)
- run: pnpm install --frozen-lockfile --filter "@commercelayer/cli-monorepo..."

- name: Resolve package
id: meta
run: node scripts/resolve-tag.mjs "$GITHUB_REF_NAME" | tee -a "$GITHUB_OUTPUT"
run: pnpm exec cl-release resolve "$GITHUB_REF_NAME" | tee -a "$GITHUB_OUTPUT"

- name: Generate release notes for this package
run: |
set -euo pipefail
node scripts/release-notes.mjs "$GITHUB_REF_NAME" "${{ steps.meta.outputs.previous_tag }}" > release-notes.md
pnpm exec cl-release notes "$GITHUB_REF_NAME" "${{ steps.meta.outputs.previous_tag }}" > release-notes.md
cat release-notes.md

- name: Create draft release
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- name: Release config is up to date
run: node scripts/gen-release-config.mjs --check
run: pnpm exec cl-release labels --check
- name: Packages are consistent
run: node scripts/check-packages.mjs

Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
set -euo pipefail
for path in $(pnpm --filter '${{ steps.select.outputs.filter }}' --filter '!@commercelayer/cli-monorepo' list --depth -1 --json | node -e 'for (const p of JSON.parse(require("fs").readFileSync(0))) console.log(require("path").relative(process.cwd(), p.path))'); do
[ "$path" = "" ] && continue
node scripts/check-manifest.mjs "$(basename "$path")" --warn
pnpm exec cl-release manifest "$(basename "$path")" --warn
done

generated:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ CL_CLI_ORGANIZATION=<org slug> CL_CLI_CLIENT_ID=<client id> CL_CLI_CLIENT_SECRET

Use an integration application of a test organization, never a production one. In CI, [integration.yml](.github/workflows/integration.yml) runs them against `cli-test-org` on pushes to `monorepo` and `main`, every night, on demand, and on pull requests that change the suites or the dependencies. They assert on the shape of the output, not on specific records.

The suites that change data (`*-write.it.ts`) only work on resources they create and delete themselves, whose names, emails and references start with `cli-it-`. `node scripts/test/live-sweep.mjs` (run before and after the suites in CI) deletes whatever an interrupted run left behind, and nothing else. The checkout and links suites also need `CL_CLI_SALES_CHANNEL_CLIENT_ID`, the client ID of a sales channel application of the organization, and are skipped without it.
The suites that change data (`*-write.it.ts`) only work on resources they create and delete themselves, whose names, emails and references start with `cli-it-`. `node packages/test-utils/bin/live-sweep.mjs` (run before and after the suites in CI) deletes whatever an interrupted run left behind, and nothing else. The checkout and links suites also need `CL_CLI_SALES_CHANNEL_CLIENT_ID`, the client ID of a sales channel application of the organization, and are skipped without it.

## Generated code

Expand Down
2 changes: 1 addition & 1 deletion commitlint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
export default {
extends: ['@commitlint/config-conventional'],
// The release commits are written by scripts/finish-version.mjs and list
// The release commits are written by `pnpm release:version` and list
// every released tag in the header, well past the length limit
ignores: [(message) => message.startsWith('chore(release): ')],
}
19 changes: 7 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,22 @@
"test:integration": "pnpm -r --no-bail --if-present --workspace-concurrency=1 test:integration",
"lint": "biome check --enforce-assist=true",
"check": "biome check --enforce-assist=true --write",
"release:version": "node scripts/finish-version.mjs",
"release:tag": "node scripts/tag.mjs",
"release:config": "node scripts/gen-release-config.mjs",
"release:publish": "node scripts/publish.mjs",
"release:version": "cl-release version",
"release:tag": "cl-release tag",
"release:config": "cl-release labels",
"release:publish": "cl-release publish",
"generate": "pnpm -r --if-present generate",
"generate-local": "pnpm -r --if-present generate-local",
"check:packages": "node scripts/check-packages.mjs",
"lint:commits": "commitlint --from origin/main --to HEAD --verbose",
"release:try": "node scripts/release-try.mjs"
"release:try": "cl-release try"
},
"devDependencies": {
"@biomejs/biome": "^2.5.14",
"@commercelayer/cli-release": "workspace:*",
"@commitlint/cli": "^21.2.3",
"@commitlint/config-conventional": "^21.2.3",
"@manypkg/get-packages": "^3.1.0",
"@pnpm/deps.graph-sequencer": "^1101.0.0",
"conventional-changelog": "^8.1.3",
"conventional-changelog-conventionalcommits": "^10.4.0",
"conventional-recommended-bump": "^12.1.0",
"git-semver-tags": "^8.0.1",
"semver": "^7.8.5"
"@manypkg/get-packages": "^3.1.0"
},
"engines": {
"node": ">=22.13"
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/.mocharc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"import": "tsx",
"require": [
"../../scripts/test/oclif-root.cjs"
"../../packages/test-utils/oclif-root.cjs"
],
"timeout": 10000
}
2 changes: 1 addition & 1 deletion packages/dev/.mocharc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"import": "tsx",
"require": [
"../../scripts/test/oclif-root.cjs"
"../../packages/test-utils/oclif-root.cjs"
],
"timeout": 10000
}
3 changes: 3 additions & 0 deletions packages/release/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"import": "tsx"
}
18 changes: 18 additions & 0 deletions packages/release/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# @commercelayer/cli-release

Private workspace package, not published. The release tooling of the monorepo, run from the root scripts and the workflows.

```sh
cl-release version [--interactive | --yes] [--base <b>] [--no-pr] [--dry-run] # pnpm release:version
cl-release tag [--base <b>] [--no-push] [--dry-run] # pnpm release:tag
cl-release try [<plugin dir>...] [--out <dir>] # pnpm release:try
cl-release labels [--check] # pnpm release:config
cl-release resolve <tag> # release.yml, publish.yml
cl-release notes <tag> [previous-tag] # release.yml
cl-release publish <tag> [--dry-run] # publish.yml (pnpm release:publish)
cl-release manifest <dir> [--against <v>] [--warn] # verify.yml, publish.yml
```

`cl-release <command> --help` shows the options of a command. It runs at the workspace root wherever it is started from.

The sources are TypeScript run through tsx (`bin/cl-release.mjs`), with no build step. Each command is a module of `src/commands`; `src/workspace.ts` has the helpers they share (packages, tags, npm). The pure parts are unit tested (`pnpm --filter @commercelayer/cli-release test`).
6 changes: 6 additions & 0 deletions packages/release/bin/cl-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env node
// The sources are TypeScript, run through tsx: no build step, as for cl-generate
import { register } from 'tsx/esm/api'

register()
await import('../src/cli.ts')
37 changes: 37 additions & 0 deletions packages/release/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@commercelayer/cli-release",
"version": "0.0.0",
"private": true,
"description": "Release tooling of the Commerce Layer CLI monorepo: versions, tags, release notes, publishing, release sandbox. Private workspace package, not published.",
"license": "MIT",
"type": "module",
"bin": {
"cl-release": "./bin/cl-release.mjs"
},
"scripts": {
"test": "mocha 'test/**/*.test.ts'",
"ts:check": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@manypkg/get-packages": "^3.1.0",
"@pnpm/deps.graph-sequencer": "^1101.0.0",
"conventional-changelog": "^8.1.3",
"conventional-changelog-conventionalcommits": "^10.4.0",
"conventional-recommended-bump": "^12.1.0",
"git-semver-tags": "^8.0.1",
"semver": "^7.8.5",
"tsx": "catalog:"
},
"devDependencies": {
"@types/chai": "catalog:",
"@types/mocha": "catalog:",
"@types/node": "catalog:",
"@types/semver": "^7.8.0",
"chai": "catalog:",
"mocha": "catalog:",
"typescript": "catalog:"
},
"engines": {
"node": ">=22.13"
}
}
19 changes: 19 additions & 0 deletions packages/release/src/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type ParseArgsOptionsConfig, parseArgs } from 'node:util'

/**
* Parses a command's arguments (node:util parseArgs, strict), printing its
* usage on --help or on an unknown option.
*/
export const parse = <O extends ParseArgsOptionsConfig>(args: string[], options: O, usage: string) => {
try {
const parsed = parseArgs({ args, options: { ...options, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true })
if ((parsed.values as { help?: boolean }).help) {
console.log(usage)
process.exit(0)
}
return parsed
} catch (error) {
console.error(`${(error as Error).message}\n\n${usage}`)
process.exit(1)
}
}
46 changes: 46 additions & 0 deletions packages/release/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* cl-release: the release tooling of the monorepo.
*
* The root scripts run it (`pnpm release:version`, …), and the workflows call
* its commands directly (`pnpm exec cl-release resolve <tag>`, …). It works
* from anywhere in the repository: it runs at the workspace root.
*/
import { execFileSync } from 'node:child_process'

const COMMANDS = {
version: ['Bump the changed packages and open the chore(release) pull request', () => import('./commands/version.ts')],
tag: ['Tag the package versions not released yet and push the tags', () => import('./commands/tag.ts')],
try: ['Install the working tree in a sandbox, packed as for npm', () => import('./commands/try.ts')],
resolve: ['Resolve a release tag to its package, as $GITHUB_OUTPUT lines', () => import('./commands/resolve.ts')],
notes: ['Write the release notes of a tag', () => import('./commands/notes.ts')],
publish: ['Publish a tag to npm, with the workspace dependencies it needs', () => import('./commands/publish.ts')],
manifest: ['Compare a package command surface with its npm version', () => import('./commands/manifest.ts')],
labels: ['Generate .github/labeler.yml (pkg:<dir> labels)', () => import('./commands/labels.ts')],
} satisfies Record<string, [string, () => Promise<{ run: (args: string[]) => Promise<void> | void }>]>

const usage = (): string => {
const width = Math.max(...Object.keys(COMMANDS).map((c) => c.length))
return [
'Usage: cl-release <command> [options]',
'',
...Object.entries(COMMANDS).map(([name, [description]]) => ` ${name.padEnd(width)} ${description}`),
'',
'cl-release <command> --help shows the options of a command.',
].join('\n')
}

const [command, ...args] = process.argv.slice(2)

if (!command || command === '--help' || command === '-h') {
console.log(usage())
process.exit(command ? 0 : 1)
}
if (!(command in COMMANDS)) {
console.error(`Unknown command '${command}'\n\n${usage()}`)
process.exit(1)
}

process.chdir(execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim())

const { run } = await COMMANDS[command as keyof typeof COMMANDS][1]()
await run(args)
42 changes: 42 additions & 0 deletions packages/release/src/commands/labels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Generates .github/labeler.yml: one `pkg:<dir>` label per package, applied
* from the files a PR touches, so a PR shows at a glance which packages it
* changes. Removes the GitHub release-notes configs (.github/release-<dir>.yml)
* once generated: the notes now come from the commits (`cl-release notes`).
*/
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { parse } from '../args.ts'
import { type Package, publicPackages } from '../workspace.ts'

const USAGE = `Usage: cl-release labels [--check]
--check exit 1 if the committed files are out of date (used in CI)`

const GITHUB = '.github'
const HEADER = '# Generated by `pnpm release:config` (cl-release labels) — do not edit by hand.\n'

export const labelerConfig = (packages: Pick<Package, 'dir' | 'path'>[]): string =>
HEADER + packages.map((pkg) => `\n'pkg:${pkg.dir}':\n - changed-files:\n - any-glob-to-any-file:\n - '${pkg.path}/**'\n`).join('')

export const run = (args: string[]): void => {
const { values } = parse(args, { check: { type: 'boolean' } }, USAGE)
const packages = publicPackages()

const files = new Map([[join(GITHUB, 'labeler.yml'), labelerConfig(packages)]])
// The release-notes configs of GitHub's generated notes, no longer used
const stale = readdirSync(GITHUB).filter((f) => /^release-.+\.yml$/.test(f) && !files.has(join(GITHUB, f)))
const outdated = [...files].filter(([path, content]) => !existsSync(path) || readFileSync(path, 'utf8') !== content).map(([path]) => path)

if (values.check) {
const problems = [...outdated, ...stale.map((f) => join(GITHUB, f))]
if (problems.length > 0) {
console.error(`Release config is out of date: ${problems.join(', ')}\nRun \`pnpm release:config\` and commit the result.`)
process.exit(1)
}
console.log(`Release config is up to date (${packages.length} packages).`)
} else {
for (const path of outdated) writeFileSync(path, files.get(path) as string)
for (const f of stale) rmSync(join(GITHUB, f))
console.log(`Wrote ${outdated.length} file(s), removed ${stale.length}, ${packages.length} packages.`)
}
}
Loading
Loading