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
11 changes: 11 additions & 0 deletions .github/workflows/release-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ on:
description: 'Version to publish (optional - will use conventional commits if not specified)'
required: false
default: ''
prerelease:
description: 'Infer or coerce the release as a next prerelease'
required: false
default: false
type: boolean
verbose:
description: 'Enable verbose logging'
required: false
Expand Down Expand Up @@ -74,11 +79,15 @@ jobs:
NPM_CONFIG_PROVENANCE: true
RELEASE_VERBOSE: ${{ inputs.verbose }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_PRERELEASE: ${{ inputs.prerelease }}
run: |
ARGS=()
if [ -n "$RELEASE_VERSION" ]; then
ARGS+=(--version "$RELEASE_VERSION")
fi
if [ "$RELEASE_PRERELEASE" = "true" ]; then
ARGS+=(--prerelease)
fi
if [ "$RELEASE_VERBOSE" = "true" ]; then
ARGS+=(--verbose)
fi
Expand All @@ -89,7 +98,9 @@ jobs:
if: always()
env:
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_PRERELEASE: ${{ inputs.prerelease }}
run: |
echo "## Release Process Summary" > "$GITHUB_STEP_SUMMARY"
echo "- Mode: publish" >> "$GITHUB_STEP_SUMMARY"
echo "- Version input: ${RELEASE_VERSION:-conventional commits}" >> "$GITHUB_STEP_SUMMARY"
echo "- Prerelease: ${RELEASE_PRERELEASE}" >> "$GITHUB_STEP_SUMMARY"
11 changes: 11 additions & 0 deletions .github/workflows/release-dry-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ on:
description: 'Version to validate (optional - will use conventional commits if not specified)'
required: false
default: ''
prerelease:
description: 'Infer or coerce the release as a next prerelease'
required: false
default: false
type: boolean
verbose:
description: 'Enable verbose logging'
required: false
Expand Down Expand Up @@ -64,11 +69,15 @@ jobs:
env:
RELEASE_VERBOSE: ${{ inputs.verbose }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_PRERELEASE: ${{ inputs.prerelease }}
run: |
ARGS=()
if [ -n "$RELEASE_VERSION" ]; then
ARGS+=(--version "$RELEASE_VERSION")
fi
if [ "$RELEASE_PRERELEASE" = "true" ]; then
ARGS+=(--prerelease)
fi
if [ "$RELEASE_VERBOSE" = "true" ]; then
ARGS+=(--verbose)
fi
Expand All @@ -79,7 +88,9 @@ jobs:
if: always()
env:
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_PRERELEASE: ${{ inputs.prerelease }}
run: |
echo "## Release Dry Run Summary" > "$GITHUB_STEP_SUMMARY"
echo "- Mode: dry-run" >> "$GITHUB_STEP_SUMMARY"
echo "- Version input: ${RELEASE_VERSION:-conventional commits}" >> "$GITHUB_STEP_SUMMARY"
echo "- Prerelease: ${RELEASE_PRERELEASE}" >> "$GITHUB_STEP_SUMMARY"
12 changes: 12 additions & 0 deletions tools/release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,28 @@ Plan a release without changing files:
```bash
pnpm run release:plan --version patch
pnpm run release:plan --version 19.3.0 --json
pnpm run release:plan --prerelease
```

Validate a prospective release without publishing:

```bash
pnpm run release:dry-run --version patch
pnpm run release:dry-run --prerelease
```

Publish a release:

```bash
pnpm turbo run release:publish --filter=@limitless-angular/release-tools -- --version patch
pnpm turbo run release:publish --filter=@limitless-angular/release-tools -- --prerelease
```

Prerelease mode infers the release level from conventional commits and uses the
`next` prerelease identifier. For example, a feature-level release from
`19.2.0` becomes `19.3.0-next.0`; the next prerelease in that train becomes
`19.3.0-next.1`.

## Pipeline

Both dry-run and publish mode use the same release pipeline:
Expand All @@ -40,6 +48,10 @@ Both dry-run and publish mode use the same release pipeline:
8. In publish mode only, commit, tag, publish the tarball to npm, push the
release commit and tag, and create the GitHub release.

Prerelease versions are published to npm with the `next` dist-tag and their
GitHub releases are marked as prereleases. Stable versions use npm's `latest`
dist-tag.

Dry-run mode restores `packages/sanity/package.json` and
`packages/sanity/CHANGELOG.md` after validation. The packed tarball remains under
`.compat/artifacts` so maintainers can inspect the exact candidate artifact.
Expand Down
21 changes: 16 additions & 5 deletions tools/release/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function runCli(args = hideBin(process.argv)) {
}),
(argv) => {
const plan = createReleasePlan({
prerelease: argv.prerelease,
versionSpecifier: argv.version,
});

Expand All @@ -49,6 +50,7 @@ export function runCli(args = hideBin(process.argv)) {
(argv) =>
runReleasePipeline({
mode: argv.mode,
prerelease: argv.prerelease,
verbose: argv.verbose,
versionSpecifier: argv.version,
}),
Expand All @@ -60,6 +62,7 @@ export function runCli(args = hideBin(process.argv)) {
(argv) =>
runReleasePipeline({
mode: releaseModes.dryRun,
prerelease: argv.prerelease,
verbose: argv.verbose,
versionSpecifier: argv.version,
}),
Expand All @@ -71,6 +74,7 @@ export function runCli(args = hideBin(process.argv)) {
(argv) =>
runReleasePipeline({
mode: releaseModes.publish,
prerelease: argv.prerelease,
verbose: argv.verbose,
versionSpecifier: argv.version,
}),
Expand All @@ -91,11 +95,18 @@ function addPipelineOptions(command) {
}

function addVersionOption(command) {
return command.option('version', {
describe:
'Explicit semver version or semver increment to release, such as 19.3.0, patch, minor, or major.',
type: 'string',
});
return command
.option('version', {
describe:
'Explicit semver version or semver increment to release, such as 19.3.0, patch, minor, or major.',
type: 'string',
})
.option('prerelease', {
default: false,
describe:
'Infer or coerce the planned version to a next prerelease, such as 19.3.0-next.0.',
type: 'boolean',
});
}

if (
Expand Down
6 changes: 5 additions & 1 deletion tools/release/src/pipeline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function runReleasePipeline(options = {}) {
capture: commandCapture,
now: options.now,
paths: options.paths,
prerelease: options.prerelease,
versionSpecifier: options.versionSpecifier,
});
let snapshot;
Expand All @@ -88,7 +89,10 @@ export function runReleasePipeline(options = {}) {
if (mode === releaseModes.publish) {
publishSideEffectsStarted = true;
commitRelease(plan, { run: commandRun });
publishTarball(artifact.tarballPath, { run: commandRun });
publishTarball(artifact.tarballPath, {
npmDistTag: plan.npmDistTag,
run: commandRun,
});
pushRelease({ run: commandRun });
createGitHubRelease(plan, { env: options.env, run: commandRun });
}
Expand Down
59 changes: 57 additions & 2 deletions tools/release/src/pipeline.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,61 @@ test('publish mode validates before starting release side effects', () => {
}
});

test('publish mode tags npm and GitHub prereleases', () => {
const fixture = createReleaseFixture();
const commands = [];

try {
const result = runReleasePipeline({
artifact: {
readTarballPackageJson() {
return {
name: '@limitless-angular/sanity',
version: '1.1.0-next.0',
};
},
tarballPath: '/tmp/release.tgz',
},
capture: createCapture({
gitLog:
'abc1234\x01feat(sanity): add release validation\x01\x01Alfonso\x02',
}),
env: { GITHUB_TOKEN: 'token' },
mode: releaseModes.publish,
now: new Date('2026-06-08T00:00:00.000Z'),
paths: fixture.paths,
prerelease: true,
run: recordCommand(commands),
});

assert.equal(result.published, true);
assert.equal(result.plan.nextVersion, '1.1.0-next.0');
assert.equal(result.plan.npmDistTag, 'next');

const npmPublish = commands.find(
({ command, args }) => command === 'npm' && args[0] === 'publish',
);
assert.deepEqual(npmPublish?.args, [
'publish',
'/tmp/release.tgz',
'--access',
'public',
'--registry',
'https://registry.npmjs.org',
'--tag',
'next',
]);

const githubRelease = commands.find(
({ command, args }) =>
command === 'gh' && args[0] === 'release' && args[1] === 'create',
);
assert.ok(githubRelease?.args.includes('--prerelease'));
} finally {
fixture.cleanup();
}
});

test('artifact version mismatch fails before publish side effects', () => {
const fixture = createReleaseFixture();
const commands = [];
Expand Down Expand Up @@ -159,14 +214,14 @@ function createReleaseFixture() {
};
}

function createCapture() {
function createCapture({ gitLog = '' } = {}) {
return (command, args) => {
if (command === 'git' && args[0] === 'rev-parse') {
return '';
}

if (command === 'git' && args[0] === 'log') {
return '';
return gitLog;
}

throw new Error(`Unexpected command: ${command} ${args.join(' ')}`);
Expand Down
Loading
Loading