feat(repo): new semantic versioning and CD process#363
feat(repo): new semantic versioning and CD process#363Charles Hudson (phobetron) wants to merge 2 commits into
Conversation
20ecb27 to
0e1298a
Compare
Replace the `commitizen`/`cz-vinyl` workflow with Commitlint-backed Conventional Commits. Commit messages now require scopes, local `commit-msg` validation, CI checks for PR titles and pushed `main` commits, and an interactive prompt for `pnpm commit` and plain interactive `git commit`. Add Release Please manifest mode as the source of truth for independent package releases. The manifest covers all public NPM packages plus Android and Swift, uses package-specific components and tags, and keeps one shared release PR titled `chore(repo): release packages`. Bootstrap the first semantic release away from the old alpha line. The initial generated releases are forced to `1.0.0` for GA NPM packages and `1.0.0-beta.0` for Android and Swift. Package-local `CHANGELOG.md` seeds make this PR touch every configured release path. Add a small Release Please runner with a `native-bridge-impact` plugin. Shared releasable runtime changes in the bridge/core path synthesize Android and Swift patch candidates. Real `android` and `swift` commits can still drive their own minor or major bumps. Update publishing to run only from released package tags. NPM publishing resolves package-specific Release Please tags, sorts released packages in workspace dependency order, rewrites package READMEs for their release tag, skips already-published versions, and publishes only to GitHub Packages. Android and Swift publishing now require `optimization-android-v*` and `optimization-swift-v*` tags. They derive versions from those tags and stamp native bridge builds with the released version. Add native `VERSION` files, Release Please config, package changelog seeds, release self-checks, and package-version build helpers so published bundles can fall back to `package.json` versions outside release-tag builds. Refresh `CONTRIBUTING.md` with the new contributor and maintainer flow: scoped commit expectations, squash-merge PR title rules, release impact guidance, Release Please ownership, publishing behavior, and troubleshooting notes. [[NT-3524](https://contentful.atlassian.net/browse/NT-3524)]
0e1298a to
cd94aca
Compare
| @@ -0,0 +1 @@ | |||
| 0.1.0-alpha17 | |||
There was a problem hiding this comment.
Is here should not match .release-please-manifest.json file?
There was a problem hiding this comment.
You're right, I missed that mobile has their own versions when I updated after alpha 18 was released.
| const JIRA_BASE_URL = 'https://contentful.atlassian.net/browse' | ||
| const JIRA_FOOTER_REGEX = | ||
| /^\[\[[A-Z][A-Z0-9]+-\d+\]\(https:\/\/contentful\.atlassian\.net\/browse\/[A-Z][A-Z0-9]+-\d+\)\]$/imu | ||
| const JIRA_TICKET_REGEX = /(^|[^A-Z0-9])([A-Z][A-Z0-9]+-\d+)(?=$|[^A-Z0-9])/iu |
There was a problem hiding this comment.
I was considering using something like process.env.JIRA_BASE_URL instead, in case anyone wanted to fork the repo.
There was a problem hiding this comment.
If someone forks, they could makes this change in their fork 😜 This script is something for us when working on it to make it easier to enter in a good commit message. If anyone on the outside works on it, it would be a bit weird for them to need to tie it to any Jira ticket. According to commitlint the footer should be optional, so it can be removed or omitted. Not sure what this script does if it can't find the ticket number; I didn't test it that way. Either way, it isn't anything blocking at all, just a maintainer helper.
| @@ -0,0 +1,46 @@ | |||
| const scopes = [ | |||
There was a problem hiding this comment.
I think would be better to use commitlint.config.ts instead, favoring what AGENTS.md says:
Write new JavaScript/Node tooling as TypeScript whenever the repo toolchain can run it; do not add .js or .mjs scripts when a .ts script is practical.
There was a problem hiding this comment.
I can't recall whether commitlint supports that. I know some tools we may use don't support TS configs, or have buggy behavior in TS configs. Will need to look into it.
| exec </dev/tty | ||
| pnpm exec tsx scripts/prepare-commit-message.ts "$1" |
There was a problem hiding this comment.
I think we would have a problem for those cases git (or the user) has already supplied a commit message, so we mustn't overwrite it. (e.g. Signed-off-by)
We should have a guard like, before pnpm exec:
if [ -s "$1" ] && grep -vq '^#' "$1"; then
exit 0
fiSo it will skip if an earlier hook or tool already wrote a real message into $1, also grep succeeds when at least one line is not a # comment.
There was a problem hiding this comment.
I thought the script handled that. I know it skipped when I amended. Can always check this locally fairly easily.
| "packages/universal/api-schemas": { | ||
| "component": "optimization-api-schemas", | ||
| "package-name": "@contentful/optimization-api-schemas", | ||
| "release-as": "1.0.0", |
There was a problem hiding this comment.
release-as here is sticky — it will freeze all releases at the bootstrap version until it's removed.
Every package in this config pins "release-as" (1.0.0 for the NPM packages, 1.0.0-beta.0 for native, lines 11–78). Unlike a one-shot Release-As: commit footer, a release-as key in release-please-config.json is re-applied on every run of Release Please, not just the first. The official manifest docs are explicit about this:
"manually set next version to be "1.2.3" ignoring conventional commits. […] Note: once the release PR is merged you should either remove this or update it to a higher version. Otherwise subsequent
manifest-prruns will continue to use this version even though it was already set in the last release."
— https://github.com/googleapis/release-please/blob/main/docs/manifest-releaser.md
Concretely, after the bootstrap release cuts 1.0.0:
- Someone lands a
feat(...). - The next release PR still forces
release-as: 1.0.0→ identical to the already-released version → no version bump, no changelog entry. publish-npm.yaml's "Resolve unpublished targets" step sees1.0.0already published and skips it.
Net effect: releases silently freeze at the bootstrap version and never advance, with no error surfaced anywhere.
Notably, CONTRIBUTING.md in this same PR already describes the correct pattern — "the maintainer must use a temporary per-package release-as override before the release PR is created" — but the committed config bakes the override in permanently, and nothing in the PR removes it after bootstrap.
Requested change (pick one):
- Preferred: remove the
release-askeys from this file and instead bootstrap the first1.0.0via a one-shotRelease-As: 1.0.0footer on a commit / the release PR (or a temporary override PR that is reverted once the first release PR is open). This keeps the committed config on conventional-commit-derived bumps. - If the keys must land here for the initial cut: add an explicit, blocking follow-up step to the maintainer checklist — "Immediately after the bootstrap release PR is created, open a PR deleting all
release-askeys fromrelease-please-config.json(or set each to"") so conventional-commit bumps resume." — and ideally track it so it can't be forgotten.
Per-package empty string ("release-as": "") also works to revert an individual package to conventional-commit bumping, per the same docs.
There was a problem hiding this comment.
I specifically noted this in the message I left about this PR. I specifically said these would need to be removed in a follow-up ticket. This is why I don't like AI PR reviews.
Tim Beyer (TimBeyer)
left a comment
There was a problem hiding this comment.
Follow-up review — release/CD infrastructure
Four inline comments below (prerelease dist-tag, missing concurrency group, self-check not in CI, fragile commitlint imports). One additional finding that has no diff line to anchor to:
CODEOWNERS gap + stale commitizen artifacts.
release-please-config.jsonand.release-please-manifest.json— the files that decide every version bump — have noCODEOWNERSrule, while.github/**,/package.json, and/.husky/**all do. The files that control releases should require an owning reviewer.CODEOWNERSstill lists/.czrc(deleted in this PR) and/.czvinylrc.yaml, and.czvinylrc.yamlitself is still present on the branch even though commitizen/cz-vinyl are fully removed. Please delete the stale file and the two deadCODEOWNERSrules, and add rules for the two Release Please config files.
(The sticky release-as issue is tracked in a separate inline comment.)
| inputs.publish) | ||
| run: | | ||
| pnpm -r ${{ steps.npm_package_targets.outputs.pnpm_filters }} publish --access public --no-git-checks | ||
| pnpm -r ${{ steps.unpublished_targets.outputs.pnpm_filters }} publish --access public --no-git-checks |
There was a problem hiding this comment.
Prerelease packages will be published under the latest dist-tag.
@contentful/optimization-react-native is configured as a beta prerelease (release-as: 1.0.0-beta.0, prerelease: true in release-please-config.json) but it is not private, so it resolves as a public npm target and flows through this publish step. This command passes no --tag.
npm's default dist-tag is latest, and npm publish has no special handling for semver prereleases — publishing 1.0.0-beta.0 with no --tag sets latest -> 1.0.0-beta.0, so a plain npm install @contentful/optimization-react-native resolves the beta. The sharper failure comes later: once RN ships a stable 1.0.0 and then a 1.1.0-beta.0, the new beta overwrites latest and silently serves a prerelease to everyone doing a plain install.
Requested change: derive the dist-tag from the version — publish prerelease versions (those whose release_version contains -) with --tag next (or beta) and only stable versions with latest. The resolved release_version is already available per target in the earlier steps, so the --tag can be selected per package.
There was a problem hiding this comment.
That's a good find.
| issues: write | ||
| pull-requests: write | ||
|
|
||
| on: |
There was a problem hiding this comment.
This workflow has no concurrency group, so two pushes to main can race.
publish-npm.yaml guards itself with a concurrency group, but this workflow does not. Two merges to main in quick succession (a normal PR merge followed by the release-PR merge, for example) can start two release-please runs concurrently, racing on the same shared release PR and on tag/release creation.
Requested change: add a serialized concurrency group, e.g.
concurrency:
group: release-please-${{ github.ref }}
cancel-in-progress: falsecancel-in-progress: false (not true) so an in-flight release/tag creation is never cancelled mid-run.
There was a problem hiding this comment.
Also a good find.
| "pm2:stop:all": "pm2 stop all", | ||
| "prepare": "husky", | ||
| "release:please": "tsx ./scripts/release-please.ts", | ||
| "release:self-check": "pnpm exec tsx ./scripts/list-npm-package-targets.ts self-check && pnpm exec tsx ./scripts/release-please.ts self-check", |
There was a problem hiding this comment.
release:self-check is never run in CI.
This script wires up the deterministic self-checks in both release scripts (npm target resolution + native-bridge synthesis), and CONTRIBUTING.md tells maintainers to run it before changing Release Please config. But nothing enforces it — a broken config, tag regex, or target-resolution change would only surface at release time.
Requested change: run pnpm release:self-check in CI (either a step in the commitlint/pipeline workflow or its own tiny job). It's fast and pure, so it's cheap insurance for a system whose whole purpose is preventing bad releases.
There was a problem hiding this comment.
If it is small and fast, being a step alongside something like the commitlint check could be OK.
| @@ -0,0 +1,261 @@ | |||
| import { input } from '@commitlint/prompt/lib/input.js' | |||
| import InputCustomPrompt from '@commitlint/prompt/lib/inquirer/InputCustomPrompt.js' | |||
There was a problem hiding this comment.
These are unexported internal paths of @commitlint/prompt.
@commitlint/prompt/lib/input.js and @commitlint/prompt/lib/inquirer/InputCustomPrompt.js are internal module paths, and SelectableInputPrompt overrides onKeyPress2 / render — private API surface. Any @commitlint/prompt minor bump can move or rename these and silently break pnpm commit and the prepare-commit-msg hook. This is compounded by pairing a standalone inquirer@^9 + @types/inquirer@^9 against commitlint 21, which vendors its own inquirer — a source of runtime/type drift.
minimumReleaseAgeExclude pins the exact patch today, which helps, but Renovate will eventually propose a bump.
Requested change: at minimum add a comment documenting this internal-API coupling, and ideally a smoke test that exercises prepare-commit-message.ts (its self-check covers the pure helpers but not the prompt wiring) so a dependency bump fails a check instead of breaking commits locally.
There was a problem hiding this comment.
I don't see this as anything that needs to be changed anytime soon. The functionality is nice, and it's only contributor/maintainer sugar, not anything that actually ships. Adding tests and such around it is a lot of overkill, and removing the functionality if we can only use it by reaching into internals would make it more annoying to use the CLI for building good commit messages.
Replace the
commitizen/cz-vinylworkflow with Commitlint-backed Conventional Commits. Commit messages now require scopes, localcommit-msgvalidation, CI checks for PR titles and pushedmaincommits, and an interactive prompt forpnpm commitand plain interactivegit commit.Add Release Please manifest mode as the source of truth for independent package releases. The manifest covers all public NPM packages plus Android and Swift, uses package-specific components and tags, and keeps one shared release PR titled
chore(repo): release packages.Bootstrap the first semantic release away from the old alpha line. The initial generated releases are forced to
1.0.0for GA NPM packages and1.0.0-beta.0for Android and Swift. Package-localCHANGELOG.mdseeds make this PR touch every configured release path.Add a small Release Please runner with a
native-bridge-impactplugin. Shared releasable runtime changes in the bridge/core path synthesize Android and Swift patch candidates. Realandroidandswiftcommits can still drive their own minor or major bumps.Update publishing to run only from released package tags. NPM publishing resolves package-specific Release Please tags, sorts released packages in workspace dependency order, rewrites package READMEs for their release tag, skips already-published versions, and publishes only to GitHub Packages.
Android and Swift publishing now require
optimization-android-v*andoptimization-swift-v*tags. They derive versions from those tags and stamp native bridge builds with the released version.Add native
VERSIONfiles, Release Please config, package changelog seeds, release self-checks, and package-version build helpers so published bundles can fall back topackage.jsonversions outside release-tag builds.Refresh
CONTRIBUTING.mdwith the new contributor and maintainer flow: scoped commit expectations, squash-merge PR title rules, release impact guidance, Release Please ownership, publishing behavior, and troubleshooting notes.[NT-3524]