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/clever-rings-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stephansama/multipublish": patch
---

Updated jsr to have auth token. fail without error for unscoped packages
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for JSR publish-related code and unscoped package handling
rg -n "unscoped|jsr.*publish|publish.*jsr|tokenEnvironmentKey|JSR_AUTH_TOKEN" --type ts --type js -C 3

Repository: stephansama/packages

Length of output: 4830


🏁 Script executed:

#!/bin/bash
# Read the publish.ts file to see the full JSR publish logic
head -120 core/multipublish/src/publish.ts | tail -60

Repository: stephansama/packages

Length of output: 1711


🏁 Script executed:

#!/bin/bash
# Read the JSR-specific logic to see if there's validation for unscoped packages
sed -n '50,80p' core/multipublish/src/publish.ts

Repository: stephansama/packages

Length of output: 1019


🏁 Script executed:

#!/bin/bash
# Look for JSR module and check for scope/unscoped package handling
find . -name "*jsr*" -type f | head -20

Repository: stephansama/packages

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Read the JSR module to check for unscoped package handling and warnings
cat -n core/multipublish/src/jsr.ts

Repository: stephansama/packages

Length of output: 3811


🏁 Script executed:

#!/bin/bash
# Check the JSR publish test to understand expected behavior
cat -n core/multipublish/src/publish.test.ts | sed -n '153,200p'

Repository: stephansama/packages

Length of output: 1029


🏁 Script executed:

#!/bin/bash
# Check if there are any tests or logic for unscoped packages
rg -n "scope" core/multipublish/src/jsr.ts core/multipublish/src/publish.ts -C 2

Repository: stephansama/packages

Length of output: 867


🏁 Script executed:

#!/bin/bash
# Get more context around the npm scope validation and check if JSR has similar logic
sed -n '40,90p' core/multipublish/src/publish.ts

Repository: stephansama/packages

Length of output: 1613


🏁 Script executed:

#!/bin/bash
# Check if there's any validation for unscoped packages in JSR publish, and compare with NPM
sed -n '44,95p' core/multipublish/src/publish.ts

Repository: stephansama/packages

Length of output: 1521


🏁 Script executed:

#!/bin/bash
# Get the complete NPM scope validation and error handling
sed -n '110,135p' core/multipublish/src/publish.ts

Repository: stephansama/packages

Length of output: 726


Clarify the changelog description; silent-failure behavior for unscoped packages is confirmed in the NPM publish path.

The phrase "fail without error for unscoped packages" is grammatically awkward and ambiguous. More importantly, this silent-failure behavior is confirmed in the codebase at the NPM publish handler (lines 125–129 of publish.ts): when a package is unscoped, console.error() logs a diagnostic but the function returns without throwing, allowing the CI/workflow to continue appearing successful.

Reword for clarity and specify which platform(s) this applies to:

-Updated jsr to have auth token. fail without error for unscoped packages
+Updated NPM and JSR publish to support auth tokens via configurable environment keys. Note: NPM publishing fails silently for unscoped packages (logs error but does not throw).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Updated jsr to have auth token. fail without error for unscoped packages
---
`@stephansama/multipublish`: patch
---
Updated NPM and JSR publish to support auth tokens via configurable environment keys. Note: NPM publishing fails silently for unscoped packages (logs error but does not throw).
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/clever-rings-cheer.md at line 5, Reword the changeset summary to
clearly state that the package publish step for unscoped packages on the NPM
publish path logs an error but returns without throwing (causing CI/workflows to
continue as successful); reference that this silent-failure occurs where the NPM
publish handler uses console.error() and returns (publish.ts NPM publish
handler), and explicitly call out the affected platform as "npm publishing" (or
"npm registry publish") and that an auth token was added to jsr.

8 changes: 8 additions & 0 deletions .config/.multipublishrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
{
"$schema": "../node_modules/@stephansama/multipublish/config/schema.json",
"platforms": [
[
"jsr",
{
"experimentalUpdateCatalogs": true,
"experimentalGenerateJSR": true,
"defaultExclude": ["!config"]
}
],
[
"npm",
{
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ env:
ZX_VERBOSE: true
DO_NOT_TRACK: 1
NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
JSR_AUTH_TOKEN: ${{secrets.JSR_AUTH_TOKEN}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

JSR_AUTH_TOKEN is exposed to all jobs and is redundant when OIDC is configured.

Two observations:

  1. id-token: write is already set at the workflow level, which is the permission JSR uses for OIDC-based authentication in GitHub Actions. This means the --token argument will never be needed for this specific workflow's CI runs — JSR authenticates automatically via OIDC, making JSR_AUTH_TOKEN unused in practice here (the env var will be undefined unless the secret is explicitly set, and the code handles that gracefully). The secret is useful for non-OIDC environments (e.g., local dev), but that context doesn't apply to this workflow file.

  2. Setting JSR_AUTH_TOKEN at the top-level env block exposes it to the test job, which has no need for it. Scoping it to the release job (matching where publishing actually occurs) would be more aligned with least-privilege, though this is consistent with the existing pattern for NODE_AUTH_TOKEN and other tokens in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml at line 13, Remove the top-level exposure of
JSR_AUTH_TOKEN: since the workflow already sets id-token: write for OIDC, drop
JSR_AUTH_TOKEN from the global env or move it into the specific release job's
env; update the env declaration to remove JSR_AUTH_TOKEN at the workflow level
and add it only to the release job (where publishing occurs) so test and other
jobs no longer receive the secret, keeping behavior for non-OIDC/local runs
intact by scoping the secret to the release job.

CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
TURBO_TOKEN: ${{secrets.TURBO_TOKEN}}
Expand Down
2 changes: 2 additions & 0 deletions core/multipublish/src/jsr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe("jsr", () => {
defaultInclude: ["src"],
experimentalGenerateJSR: false,
experimentalUpdateCatalogs: false,
tokenEnvironmentKey: "test",
};

updateIncludeExcludeList(jsrConfig, appConfig);
Expand All @@ -72,6 +73,7 @@ describe("jsr", () => {
defaultInclude: ["new-include"],
experimentalGenerateJSR: false,
experimentalUpdateCatalogs: false,
tokenEnvironmentKey: "test"
};

updateIncludeExcludeList(jsrConfig, appConfig);
Expand Down
40 changes: 39 additions & 1 deletion core/multipublish/src/publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ describe("publish", () => {
);
});

it("should publish to jsr", async () => {
it("should publish to jsr without token", async () => {
vi.stubEnv("JSR_AUTH_TOKEN", "");
vi.mocked(mocks.loadConfig).mockResolvedValue({
config: {
exports: "index.ts",
Expand Down Expand Up @@ -183,5 +184,42 @@ describe("publish", () => {
{ stdio: "inherit" },
);
});

it("should publish to jsr with token", async () => {
const token = "test-token";
vi.stubEnv("JSR_AUTH_TOKEN", token);

vi.mocked(mocks.loadConfig).mockResolvedValue({
config: {
exports: "index.ts",
name: "@scope/pkg",
version: "1.0.0",
},
filename: "/path/to/pkg/jsr.json",
});

const pkg = {
dir: "/path/to/pkg",
packageJson: {
name: "@scope/pkg",
version: "1.0.0",
},
relativeDir: "./pkg",
};

const platform = "jsr";

await publishPlatform(pkg, platform);

expect(mocks.writeFile).toHaveBeenCalledWith(
"/path/to/pkg/jsr.json",
expect.any(String),
);

expect(mocks.execSync).toHaveBeenCalledWith(
`pnpm dlx jsr publish --allow-dirty --allow-slow-types --token ${token}`,
{ stdio: "inherit" },
);
});
Comment on lines +188 to +223
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Token interpolated directly into shell command in publish.ts — potential shell injection.

The test at line 220 confirms publish.ts builds the execSync string by raw interpolation:

pnpm dlx jsr publish --allow-dirty --allow-slow-types --token ${token}

execSync spawns a subshell; if JSR_AUTH_TOKEN ever contains shell metacharacters (spaces, ;, &&, $(...), etc.), it will misbehave or inject commands. While CI-provisioned tokens are typically alphanumeric, the implementation in publish.ts provides no sanitisation or quoting.

Consider switching to execFileSync with an explicit argv array in publish.ts, which bypasses the shell entirely:

// In publish.ts (not in the test file)
const args = ["dlx", "jsr", "publish", "--allow-dirty", "--allow-slow-types"];
if (token) args.push("--token", token);
cp.execFileSync("pnpm", args, { stdio: "inherit" });

The test assertion would then change to match execFileSync instead of execSync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/multipublish/src/publish.test.ts` around lines 188 - 223, The test shows
publish.ts interpolates JSR_AUTH_TOKEN directly into an execSync shell string
(in publishPlatform), which risks shell injection; change publishPlatform to
call child_process.execFileSync("pnpm", args, { stdio: "inherit" }) instead of
execSync by building an argv array like
["dlx","jsr","publish","--allow-dirty","--allow-slow-types", "--token", token]
only when token exists, and update the test to expect mocks.execFileSync with
the same args/stdio call; ensure the unique symbol publishPlatform (in
publish.ts) is updated and any mocks use execFileSync instead of execSync.

});
});
11 changes: 8 additions & 3 deletions core/multipublish/src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,18 @@ export async function publishPlatform(
}
}

const authToken = process.env[config.tokenEnvironmentKey];

await util.chdir(pkg.dir, () => {
cp.execSync(
[
jsrPublishCommand[packageManager],
"--allow-dirty",
isDryRun && "--dry-run",
config.allowSlowTypes && "--allow-slow-types",
isDryRun && "--dry-run",
authToken && `--token ${authToken}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Directly interpolating authToken into the command string can lead to a command injection vulnerability if the token contains shell metacharacters. It is generally safer to pass sensitive information like API tokens via environment variables to the child process. The jsr publish command supports reading the token from the JSR_API_TOKEN environment variable. Consider passing the token via the env option of cp.execSync instead of as a command-line argument.

]
.filter((x) => x)
.filter((x): x is string => !!x)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Filter type guard inconsistency with the npm path.

The JSR path uses a proper type guard .filter((x): x is string => !!x), while the npm path at Line 160 still uses the untyped .filter((x) => x). Consider applying the same type-safe filter there for consistency.

♻️ Proposed fix
// Line 160 (npm path)
-					.filter((x) => x)
+					.filter((x): x is string => !!x)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/multipublish/src/publish.ts` at line 89, The npm branch in
core/multipublish/src/publish.ts uses an untyped filter `.filter((x) => x)`
which is inconsistent with the JSR path's type guard `.filter((x): x is string
=> !!x)`; change the npm-side filter to a proper type guard (e.g., `.filter((x):
x is string => !!x)`) so downstream code that expects strings has correct
typings—locate the npm path where `.filter((x) => x)` is used and replace it
with the typed predicate.

.join(" "),
{ stdio: "inherit" },
);
Expand Down Expand Up @@ -120,7 +123,9 @@ export async function publishPlatform(

const scope = pkg.packageJson.name.split("/").at(0);
if (!scope?.startsWith("@")) {
throw new Error("scope must start with `@` symbol");
return console.error(
"scope does not start with @ symbol. aborting.",
);
Comment on lines +126 to +128
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Changing from throw new Error to return console.error alters the error handling behavior. Throwing an error provides a clearer indication of a critical failure and propagates the error up the call stack, which can be important for robust error management and debugging. Returning after logging an error might lead to silent failures that are harder to detect and diagnose.

throw new Error("scope must start with `@` symbol");

}
Comment on lines +126 to 129
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

return console.error(...) silently swallows the failure — callers receive a resolved Promise<void> instead of a rejection.

console.error returns undefined. So return console.error(...) is equivalent to calling console.error(...) and then return;, which resolves the enclosing async function's promise successfully. The pre-existing throw new Error(...) would have rejected the promise and surfaced the failure to callers. With this change, if a package name has no @ scope, the publish is silently skipped with no observable error propagation — callers have no way to distinguish this from a successful publish.

🐛 Proposed fix
-					return console.error(
-						"scope does not start with @ symbol. aborting.",
-					);
+					console.error("scope does not start with @ symbol. aborting.");
+					throw new Error("scope does not start with @ symbol. aborting.");

Or, if silent continuation is intentional (best-effort multi-package publish), document this explicitly and ensure the caller accounts for no-op returns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/multipublish/src/publish.ts` around lines 126 - 129, The current change
replaces a thrown error with `return console.error(...)`, which causes the
surrounding async function to resolve instead of reject and silently swallow
failures; restore proper error propagation by replacing `return
console.error("scope does not start with @ symbol. aborting.")` with `throw new
Error("scope does not start with @ symbol. aborting.")` (or, if you
intentionally want a non-fatal best-effort publish, explicitly log the message
and return a documented no-op result or an explicit rejected Promise so callers
can distinguish behavior); update the code path in the publish routine where the
scope check occurs to either rethrow the Error or make the silent continuation
explicit and documented.


const npmrcFile =
Expand Down
1 change: 1 addition & 0 deletions core/multipublish/src/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("schema", () => {
allowSlowTypes: false,
experimentalGenerateJSR: false,
experimentalUpdateCatalogs: false,
tokenEnvironmentKey: "JSR_AUTH_TOKEN",
},
],
];
Expand Down
1 change: 1 addition & 0 deletions core/multipublish/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const jsrPlatformOptionsSchema = z.object({
defaultInclude: z.array(z.string()).optional(),
experimentalGenerateJSR: z.boolean().default(false),
experimentalUpdateCatalogs: z.boolean().default(false),
tokenEnvironmentKey: z.string().default("JSR_AUTH_TOKEN"),
});

export type NpmPlatformOptionsSchema = z.infer<typeof npmPlatformOptionsSchema>;
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"scripts:pkg-pr-new": "node ./scripts/pkg-pr-new.js",
"test": "turbo test",
"pretest:ci": "pnpm run scripts:generate-examples",
"test:ci": "vitest --run --coverage --reporter=junit --outputFile=test-report.junit.xml",
"test:ci": "vitest --run --coverage",
"test:ui": "vitest --ui",
"version": "changeset version && pnpm install --lockfile-only"
},
Expand Down Expand Up @@ -87,9 +87,9 @@
"vitest": "catalog:vitest",
"yaml-eslint-parser": "^1.3.2"
},
"packageManager": "pnpm@10.29.3",
"devDependencies": {
"actions-up": "^1.11.0",
"taze": "^19.9.2"
}
},
"packageManager": "pnpm@10.29.3"
}
2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ catalog:
"@types/react": ^19.2.14
"@types/vfile": ^4.0.0
"@types/yargs": ^17.0.35
actions-up: ^1.11.0
deepmerge: "^4.3.1"
es-toolkit: 1.43.0
astro: 5.9.3
Expand All @@ -63,6 +64,7 @@ catalog:
prettier: ^3.8.1
react: 19.2.0
remark: ^15.0.1
taze: ^19.9.2
tsdown: 0.15.12
tsx: 4.21.0
typescript: 5.9.3
Expand Down
2 changes: 1 addition & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default defineConfig({
provider: "v8",
reporter: ["html", "json", "text"],
},
outputFile: { junit: "./coverage/test-report.junit.xml" },
outputFile: { junit: "./test-report.junit.xml" },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. Check if test-report.junit.xml at root is covered by .gitignore
echo "=== Checking .gitignore for test-report patterns ==="
if [ -f .gitignore ]; then
  rg -n "test-report" .gitignore || echo "No test-report entry found in .gitignore"
else
  echo ".gitignore not found"
fi

echo ""
echo "=== Checking for broad patterns that might cover root-level test files ==="
rg -n "^\*\.\(xml\|junit\)" .gitignore || echo "No broad XML/junit patterns found"

echo ""
echo "=== Full .gitignore content for context ==="
cat .gitignore

echo ""
echo "=== Checking CI workflow files for junit path references ==="
if [ -d .github/workflows ]; then
  rg -n "test-report\|junit\|coverage" .github/workflows/ || echo "No junit/coverage references found in CI workflows"
else
  echo ".github/workflows directory not found"
fi

Repository: stephansama/packages

Length of output: 850


🏁 Script executed:

#!/bin/bash
# Check for other CI system config files
echo "=== Searching for CI configuration files ==="
fd -type f -name ".gitlab-ci.yml" -o -name "*.circleci*" -o -name "bitbucket-pipelines.yml" -o -name ".travis.yml" 2>/dev/null || echo "No other CI configs found via fd"

echo ""
echo "=== Searching for any 'test' or 'junit' references in .github/ recursively ==="
if [ -d .github ]; then
  find .github -type f | head -20
  echo ""
  rg -i "test|junit|artifact" .github/ || echo "No artifact/test references in .github"
else
  echo ".github directory not found"
fi

echo ""
echo "=== Checking if .github/workflows exists and listing files ==="
if [ -d .github/workflows ]; then
  ls -la .github/workflows/
else
  echo ".github/workflows not found"
fi

echo ""
echo "=== Broader search for 'test-report' in entire repo (excluding node_modules) ==="
rg -i "test-report\|junit\.xml" --glob='!node_modules' . 2>/dev/null | head -20

Repository: stephansama/packages

Length of output: 1309


Update .gitignore and CI workflow for new junit report location.

The junit report path change from ./coverage/test-report.junit.xml to ./test-report.junit.xml requires two fixes:

  1. Add test-report.junit.xml to .gitignore — the new root-level file is not currently ignored (only **/coverage/ is covered).
  2. Update .github/workflows/release.yml — the codecov/test-results-action step still references the old path ("./coverage/test-report.junit.xml") and must be changed to "./test-report.junit.xml" to match the new output location.

Without these changes, the junit report will be committed to the repo and the CI test upload will fail.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vitest.config.ts` at line 10, The junit report path was changed in the vitest
config (see outputFile: { junit: "./test-report.junit.xml" }) but you must also
1) add the new root-level filename test-report.junit.xml to .gitignore so it
isn't committed, and 2) update the codecov/test-results-action invocation in
.github/workflows/release.yml to point to "./test-report.junit.xml" instead of
"./coverage/test-report.junit.xml"; make both edits so the produced report is
ignored locally and CI uploads the correct file.

projects: ["./core/*"],
reporters: ["default", "junit"],
},
Expand Down
Loading