Compare commits

...

6 Commits

Author SHA1 Message Date
Claude Bot
306c92528f Fix header duplication bug in checkPackageVersionExists function 2025-08-26 23:01:14 +00:00
autofix-ci[bot]
889e18f2ba [autofix.ci] apply automated fixes 2025-08-25 04:19:33 +00:00
Claude Bot
a4cc31e9c0 Clean up verbose comments and remove external references
- Remove unnecessary explanatory comments
- Clean up function documentation
- Simplify test descriptions
- Remove verbose implementation details from comments
2025-08-25 04:17:05 +00:00
Claude Bot
30f3870bce Implement Yarn's proactive approach for --tolerate-republish
- Replace reactive error parsing with proactive registry check
- When --tolerate-republish is enabled, make HEAD request to check if version exists BEFORE publishing
- Skip publishing entirely if version already exists (just like Yarn)
- Remove old isRepublishError() function and reactive error handling
- More efficient: avoids unnecessary packing/uploading when version exists
- More reliable: direct registry check instead of parsing error messages
- Update test descriptions to reflect proactive behavior
2025-08-24 23:22:49 +00:00
Claude Bot
2846bc0bb3 Refactor: move tolerate_republish to PublishConfig struct
- Remove tolerate_republish parameter from publish() function
- Add tolerate_republish field to PublishConfig struct in PackageManagerOptions
- Access flag through ctx.manager.options.publish_config.tolerate_republish
- Cleaner architecture: keeps publish config options grouped together
2025-08-24 22:55:35 +00:00
Claude Bot
b272f73e01 Add --tolerate-republish flag to bun publish command
This flag allows `bun publish` to exit with code 0 instead of code 1
when attempting to republish over an existing version number. This is
useful in automated workflows where republishing the same version
might occur and should not be treated as an error.

Implementation follows Yarn's design philosophy: only perform the
additional registry check when the flag is explicitly provided,
keeping the default fast path unchanged.

Changes:
- Add --tolerate-republish CLI flag parsing
- Implement republish error detection and tolerance logic
- Add help text and usage examples
- Add comprehensive test coverage
- Update CLI documentation

Note: Current implementation uses reactive error detection. Future
enhancement should implement Yarn's proactive registry check approach
for better efficiency when the flag is enabled.
2025-08-24 22:49:54 +00:00
5 changed files with 180 additions and 0 deletions

View File

@@ -82,6 +82,16 @@ The `--dry-run` flag can be used to simulate the publish process without actuall
$ bun publish --dry-run
```
### `--tolerate-republish`
The `--tolerate-republish` flag makes `bun publish` exit with code 0 instead of code 1 when attempting to republish over an existing version number. This is useful in automated workflows where republishing the same version might occur and should not be treated as an error.
```sh
$ bun publish --tolerate-republish
```
Without this flag, attempting to publish a version that already exists will result in an error and exit code 1. With this flag, the command will exit successfully even when trying to republish an existing version.
### `--gzip-level`
Specify the level of gzip compression to use when packing the package. Only applies to `bun publish` without a tarball path argument. Values range from `0` to `9` (default is `9`).

View File

@@ -451,6 +451,68 @@ pub const PublishCommand = struct {
NeedAuth,
};
fn checkPackageVersionExists(
allocator: std.mem.Allocator,
package_name: string,
version: string,
registry: *const Npm.Registry.Scope,
) bool {
var url_buf = std.ArrayList(u8).init(allocator);
defer url_buf.deinit();
const registry_url = strings.withoutTrailingSlash(registry.url.href);
const encoded_name = bun.fmt.dependencyUrl(package_name);
url_buf.writer().print("{s}/{s}/{s}", .{ registry_url, encoded_name, version }) catch return false;
const package_version_url = URL.parse(url_buf.items);
var response_buf = MutableString.init(allocator, 64) catch return false;
defer response_buf.deinit();
var headers = http.HeaderBuilder{};
headers.count("accept", "application/json");
var auth_buf = std.ArrayList(u8).init(allocator);
defer auth_buf.deinit();
if (registry.token.len > 0) {
auth_buf.writer().print("Bearer {s}", .{registry.token}) catch return false;
headers.count("authorization", auth_buf.items);
} else if (registry.auth.len > 0) {
auth_buf.writer().print("Basic {s}", .{registry.auth}) catch return false;
headers.count("authorization", auth_buf.items);
}
headers.allocate(allocator) catch return false;
headers.append("accept", "application/json");
if (registry.token.len > 0) {
auth_buf.clearRetainingCapacity();
auth_buf.writer().print("Bearer {s}", .{registry.token}) catch return false;
headers.append("authorization", auth_buf.items);
} else if (registry.auth.len > 0) {
auth_buf.clearRetainingCapacity();
auth_buf.writer().print("Basic {s}", .{registry.auth}) catch return false;
headers.append("authorization", auth_buf.items);
}
var req = http.AsyncHTTP.initSync(
allocator,
.HEAD,
package_version_url,
headers.entries,
headers.content.ptr.?[0..headers.content.len],
&response_buf,
"",
null,
null,
.follow,
);
const res = req.sendSync() catch return false;
return res.status_code == 200;
}
pub fn publish(
comptime directory_publish: bool,
ctx: *const Context(directory_publish),
@@ -461,6 +523,22 @@ pub const PublishCommand = struct {
return error.NeedAuth;
}
const tolerate_republish = ctx.manager.options.publish_config.tolerate_republish;
if (tolerate_republish) {
const version_without_build_tag = Dependency.withoutBuildTag(ctx.package_version);
const package_exists = checkPackageVersionExists(
ctx.allocator,
ctx.package_name,
version_without_build_tag,
registry,
);
if (package_exists) {
Output.prettyln("<yellow>warning<r>: Registry already knows about version {s}; skipping.", .{version_without_build_tag});
return;
}
}
// continues from `printSummary`
Output.pretty(
\\<b><blue>Tag<r>: {s}

View File

@@ -158,6 +158,7 @@ const publish_params: []const ParamType = &(shared_params ++ [_]ParamType{
clap.parseParam("--otp <STR> Provide a one-time password for authentication") catch unreachable,
clap.parseParam("--auth-type <STR> Specify the type of one-time password authentication (default is 'web')") catch unreachable,
clap.parseParam("--gzip-level <STR> Specify a custom compression level for gzip. Default is 9.") catch unreachable,
clap.parseParam("--tolerate-republish Don't exit with code 1 when republishing over an existing version number") catch unreachable,
});
const why_params: []const ParamType = &(shared_params ++ [_]ParamType{
@@ -218,6 +219,8 @@ registry: string = "",
publish_config: Options.PublishConfig = .{},
tolerate_republish: bool = false,
ca: []const string = &.{},
ca_file_name: string = "",
@@ -605,6 +608,9 @@ pub fn printHelp(subcommand: Subcommand) void {
\\ <d>Publish a pre-existing package tarball with tag 'next'.<r>
\\ <b><green>bun publish<r> <cyan>--tag next<r> <blue>./path/to/tarball.tgz<r>
\\
\\ <d>Publish without failing when republishing over an existing version.<r>
\\ <b><green>bun publish<r> <cyan>--tolerate-republish<r>
\\
\\Full documentation is available at <magenta>https://bun.com/docs/cli/publish<r>.
\\
;
@@ -860,6 +866,8 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
Global.crash();
};
}
cli.tolerate_republish = args.flag("--tolerate-republish");
}
// link and unlink default to not saving, all others default to

View File

@@ -79,6 +79,7 @@ pub const PublishConfig = struct {
tag: string = "",
otp: string = "",
auth_type: ?AuthType = null,
tolerate_republish: bool = false,
};
pub const Access = enum {
@@ -636,6 +637,7 @@ pub fn load(
if (cli.publish_config.auth_type) |auth_type| {
this.publish_config.auth_type = auth_type;
}
this.publish_config.tolerate_republish = cli.tolerate_republish;
if (cli.ca.len > 0) {
this.ca = cli.ca;

View File

@@ -848,3 +848,85 @@ it("$npm_lifecycle_event is accurate during publish", async () => {
]);
expect(exitCode).toBe(0);
});
describe("--tolerate-republish", async () => {
test("republishing normally fails", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("republish-fail");
const pkgJson = {
name: "republish-test-1",
version: "1.0.0",
};
await Promise.all([
rm(join(registry.packagesPath, "republish-test-1"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(packageJson, JSON.stringify(pkgJson)),
]);
// First publish should succeed
let { out, err, exitCode } = await publish(env, packageDir);
expect(exitCode).toBe(0);
expect(out).toContain("+ republish-test-1@1.0.0");
// Second publish should fail
({ out, err, exitCode } = await publish(env, packageDir));
expect(exitCode).toBe(1);
expect(err).toContain("403") || expect(err).toContain("already exists") || expect(err).toContain("cannot publish");
});
test("republishing with --tolerate-republish skips when version exists", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("republish-tolerate");
const pkgJson = {
name: "republish-test-2",
version: "1.0.0",
};
await Promise.all([
rm(join(registry.packagesPath, "republish-test-2"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(packageJson, JSON.stringify(pkgJson)),
]);
// First publish should succeed
let { out, err, exitCode } = await publish(env, packageDir);
expect(exitCode).toBe(0);
expect(out).toContain("+ republish-test-2@1.0.0");
// Second publish with --tolerate-republish should skip
({ out, err, exitCode } = await publish(env, packageDir, "--tolerate-republish"));
expect(exitCode).toBe(0);
expect(err).toContain("warning: Registry already knows about version 1.0.0; skipping.");
expect(err).not.toContain("error:");
});
test("republishing tarball with --tolerate-republish skips when version exists", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("republish-tarball");
const pkgJson = {
name: "republish-test-3",
version: "1.0.0",
};
await Promise.all([
rm(join(registry.packagesPath, "republish-test-3"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(packageJson, JSON.stringify(pkgJson)),
]);
// Create tarball
await pack(packageDir, env);
// First publish should succeed
let { out, err, exitCode } = await publish(env, packageDir, "./republish-test-3-1.0.0.tgz");
expect(exitCode).toBe(0);
expect(out).toContain("+ republish-test-3@1.0.0");
// Second publish with --tolerate-republish should skip
({ out, err, exitCode } = await publish(env, packageDir, "./republish-test-3-1.0.0.tgz", "--tolerate-republish"));
expect(exitCode).toBe(0);
expect(err).toContain("warning: Registry already knows about version 1.0.0; skipping.");
expect(err).not.toContain("error:");
});
});