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
5 changes: 5 additions & 0 deletions .changeset/funny-parks-like.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lazy-release/changesets": feat
---

Added snapshots
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,65 @@ This will:
- `--github-token <token>` - GitHub token for releases (defaults to `GITHUB_TOKEN` env var)
- `--draft` - Create GitHub releases as drafts

### 6. Snapshot Releases (Testing)

Create temporary snapshot releases for testing unpublished changes:

```bash
changeset snapshot
```

This will:
- Generate a unique version: `0.0.0-<timestamp>` (e.g., `0.0.0-1705242645`)
- Update all affected packages and their dependents
- Update internal dependencies to use exact snapshot versions
- Publish to npm with `snapshot` tag (not `latest`)
- Restore package.json files to original state
- Skip git tags and GitHub releases

**Snapshot releases are temporary and don't modify your version history.**

#### Options

- `--dry-run` - Preview what would be published without actually publishing

#### Installing Snapshots

Install snapshot releases in other projects:

```bash
npm install my-package@snapshot
```

#### Use Cases

- **Test changes before releasing**: Validate your changes in a real environment
- **Share work in progress**: Let others test your changes without a formal release
- **CI/CD testing**: Test integration with dependent projects in CI pipelines

#### Example Workflow

```bash
# 1. Make changes and create changesets
git checkout -b feature/new-api
# ... make code changes ...
changeset
# Select packages, type: feat, message: "Add new API method"

# 2. Publish snapshot for testing
changeset snapshot
# Output: Published my-package@0.0.0-1705242645 with 'snapshot' tag

# 3. Test in another project
cd ../my-app
npm install my-package@snapshot

# 4. Once testing is complete, create proper release
cd ../my-package
changeset version
changeset publish
```

## 📋 Configuration

Edit `.changeset/config.json` to customize behavior:
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import pc from "picocolors";
import { ChangesetConfig, readConfig } from "./config.js";
import { version } from "./version.js";
import { publish } from "./publish.js";
import { snapshot } from "./snapshot.js";
import { parseChangesetFile } from "./version.js";

async function findPackages(config: ChangesetConfig): Promise<Map<string, string>> {
Expand Down Expand Up @@ -329,6 +330,15 @@ program
process.exit(0);
});

program
.command("snapshot")
.description("Publish snapshot versions for testing (0.0.0-TIMESTAMP with 'snapshot' tag)")
.option("--dry-run", "Preview what would be published without actually publishing", false)
.action(async (options) => {
await snapshot({ dryRun: options.dryRun });
process.exit(0);
});

program.parse(process.argv);

async function init() {
Expand Down
89 changes: 43 additions & 46 deletions src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ async function tagExistsRemote(tag: string): Promise<boolean> {
}
}

async function publishToNpm(pkg: PackageInfo, config: ChangesetConfig) {
export async function publishToNpm(
pkg: PackageInfo,
config: ChangesetConfig,
tag: string = "latest",
) {
const detected = await detect();
if (!detected) {
console.warn(pc.yellow("Could not detect package manager. Skipping npm publish."));
Expand All @@ -189,21 +193,22 @@ async function publishToNpm(pkg: PackageInfo, config: ChangesetConfig) {
let publishCmd = "";
const access = pkg.access || config.access;
const accessFlag = access === "public" || access === "restricted" ? `--access ${access}` : "";
const tagFlag = `--tag ${tag}`;

switch (agent) {
case "npm":
publishCmd = `npm publish ${accessFlag}`.trim();
publishCmd = `npm publish ${tagFlag} ${accessFlag}`.trim();
break;
case "yarn":
case "yarn@berry":
publishCmd = `yarn publish --non-interactive ${accessFlag}`.trim();
publishCmd = `yarn publish --non-interactive ${tagFlag} ${accessFlag}`.trim();
break;
case "pnpm":
case "pnpm@6":
publishCmd = `pnpm publish --no-git-checks ${accessFlag}`.trim();
publishCmd = `pnpm publish --no-git-checks ${tagFlag} ${accessFlag}`.trim();
break;
case "bun":
publishCmd = `bun publish ${accessFlag}`.trim();
publishCmd = `bun publish ${tagFlag} ${accessFlag}`.trim();
break;
default:
console.warn(pc.yellow(`Unsupported package manager: ${agent}. Skipping npm publish.`));
Expand All @@ -212,12 +217,8 @@ async function publishToNpm(pkg: PackageInfo, config: ChangesetConfig) {

console.log(pc.dim("Publishing to npm..."));

try {
execSync(publishCmd, { cwd: pkg.dir, stdio: "inherit" });
console.log(pc.green("✔"), "Published to npm");
} catch (error) {
throw error;
}
execSync(publishCmd, { cwd: pkg.dir, stdio: "inherit" });
console.log(pc.green("✔"), "Published to npm");
}

async function createGitHubRelease(
Expand All @@ -237,48 +238,44 @@ async function createGitHubRelease(

console.log(pc.dim("Creating GitHub release..."));

try {
const { owner, repo } = getGitHubRepoInfo();
const token = githubToken || process.env.GITHUB_TOKEN;

if (!token) {
throw new Error(
"GITHUB_TOKEN environment variable is required. " +
'Create a token at https://github.com/settings/tokens with "repo" scope.',
);
}
const { owner, repo } = getGitHubRepoInfo();
const token = githubToken || process.env.GITHUB_TOKEN;

const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
tag_name: tag,
name: tag,
body: releaseNotes,
draft: draft ?? false,
prerelease: false,
}),
});
if (!token) {
throw new Error(
"GITHUB_TOKEN environment variable is required. " +
'Create a token at https://github.com/settings/tokens with "repo" scope.',
);
}

if (!response.ok) {
const error = await response.text();
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
tag_name: tag,
name: tag,
body: releaseNotes,
draft: draft ?? false,
prerelease: false,
}),
});

// GitHub returns 422 when a release already exists for the tag
if (response.status === 422) {
console.log(pc.dim(`GitHub release for ${tag} already exists. Skipping.`));
return;
}
if (!response.ok) {
const error = await response.text();

throw new Error(`GitHub API error: ${response.status} ${error}`);
// GitHub returns 422 when a release already exists for the tag
if (response.status === 422) {
console.log(pc.dim(`GitHub release for ${tag} already exists. Skipping.`));
return;
}

console.log(pc.green("✔"), "Created GitHub release");
} catch (error) {
throw error;
throw new Error(`GitHub API error: ${response.status} ${error}`);
}

console.log(pc.green("✔"), "Created GitHub release");
}

function getGitHubRepoInfo(): { owner: string; repo: string } {
Expand Down
Loading