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
21 changes: 21 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Publish on GitHub release using npm Trusted Publishing (OIDC).
# Docs: https://docs.npmjs.com/trusted-publishers
#
# Versioning: npm rejects republishing the same version. Before publish, CI sets
# the package version from the GitHub release tag (v1.5.0 → 1.5.0). If that
# version is already on the registry, the patch number is incremented until it
# is unpublished. Bump major/minor by tagging the release (for example v1.6.0).
#
# Required on npmjs.com → @veryfi/veryfi-sdk → Settings → Trusted Publisher:
# Organization or user: veryfi
# Repository: veryfi-nodejs
Expand Down Expand Up @@ -28,12 +33,25 @@ jobs:
node-version: "24"
package-manager-cache: false
- run: npm ci
- name: Set unpublished package version
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: node scripts/set-publish-version.js
- uses: actions/upload-artifact@v4
with:
name: package-version
path: |
package.json
package-lock.json

publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
name: package-version
# Intentionally omit registry-url. setup-node writes an empty
# _authToken placeholder that blocks the OIDC exchange (E404).
- uses: actions/setup-node@v6
Expand Down Expand Up @@ -121,6 +139,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
name: package-version
- uses: actions/setup-node@v6
with:
node-version: "24"
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@veryfi/veryfi-sdk",
"version": "1.4.8",
"version": "1.5.0",
"description": "Node.js module for communicating with the Veryfi OCR API",
"main": "lib/client/client.js",
"typings": "lib/types/Client.ts",
Expand Down
87 changes: 87 additions & 0 deletions scripts/next-publish-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Resolve an unpublished npm version for CI releases.
*
* Preference order:
* 1. GitHub release tag (v1.5.0 → 1.5.0) when it is valid semver
* 2. package.json version
*
* If that version is already on the registry, bump the patch until it is free.
*/

/**
* @param {string|undefined|null} value
* @returns {{major: number, minor: number, patch: number}|null}
*/
function parseSemver(value) {
if (!value || typeof value !== "string") {
return null;
}
const match = value.trim().replace(/^v/i, "").match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!match) {
return null;
}
return {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
};
}

/**
* @param {{major: number, minor: number, patch: number}} version
* @returns {string}
*/
function formatSemver(version) {
return `${version.major}.${version.minor}.${version.patch}`;
}

/**
* @param {{major: number, minor: number, patch: number}} version
* @returns {{major: number, minor: number, patch: number}}
*/
function incrementPatch(version) {
return {
major: version.major,
minor: version.minor,
patch: version.patch + 1,
};
}

/**
* @param {string} packageVersion
* @param {string|undefined|null} releaseTag
* @param {string[]|string|undefined|null} publishedVersions
* @returns {string}
*/
function resolvePublishVersion(packageVersion, releaseTag, publishedVersions) {
const fromPackage = parseSemver(packageVersion);
if (!fromPackage) {
throw new Error(`Invalid package.json version: ${packageVersion}`);
}

const candidate = parseSemver(releaseTag) || fromPackage;
const publishedList = Array.isArray(publishedVersions)
? publishedVersions
: publishedVersions
? [publishedVersions]
: [];
const published = new Set(
publishedList
.map((version) => parseSemver(version))
.filter(Boolean)
.map((version) => formatSemver(version))
);

let next = candidate;
while (published.has(formatSemver(next))) {
next = incrementPatch(next);
}
return formatSemver(next);
}

module.exports = {
parseSemver,
formatSemver,
incrementPatch,
resolvePublishVersion,
};
65 changes: 65 additions & 0 deletions scripts/set-publish-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env node
/**
* Set package.json (and package-lock.json) to an unpublished version before npm publish.
*
* Reads RELEASE_TAG or GITHUB_REF_NAME for the GitHub release tag.
* Queries the npm registry for already-published versions of this package.
*/
const {execFileSync} = require("child_process");
const path = require("path");
const {resolvePublishVersion} = require("./next-publish-version");

function readPublishedVersions(packageName) {
try {
const output = execFileSync(
"npm",
["view", packageName, "versions", "--json"],
{encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]}
);
const parsed = JSON.parse(output);
if (Array.isArray(parsed)) {
return parsed;
}
if (typeof parsed === "string") {
return [parsed];
}
return [];
} catch (error) {
const stderr = error.stderr ? String(error.stderr) : String(error.message || error);
if (/E404|404 Not Found|code E404/i.test(stderr)) {
return [];
}
console.warn("Could not read published versions from npm; treating as none.");
console.warn(stderr.trim());
return [];
}
}

function main() {
const packageJsonPath = path.join(__dirname, "..", "package.json");
const pkg = require(packageJsonPath);
const releaseTag = process.env.RELEASE_TAG || process.env.GITHUB_REF_NAME || "";
const published = readPublishedVersions(pkg.name);
const nextVersion = resolvePublishVersion(pkg.version, releaseTag, published);

console.log(`package.json version: ${pkg.version}`);
console.log(`release tag: ${releaseTag || "(none)"}`);
console.log(`latest published: ${published[published.length - 1] || "(none)"}`);
console.log(`publish version: ${nextVersion}`);

if (nextVersion === pkg.version) {
console.log("package.json already matches an unpublished version");
return;
}

execFileSync("npm", ["version", nextVersion, "--no-git-tag-version", "--allow-same-version"], {
stdio: "inherit",
cwd: path.join(__dirname, ".."),
});
}

if (require.main === module) {
main();
}

module.exports = {readPublishedVersions, main};
40 changes: 40 additions & 0 deletions test/next-publish-version.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const {describe, expect, test} = require("@jest/globals");
const {parseSemver, resolvePublishVersion} = require("../scripts/next-publish-version");

describe("parseSemver", () => {
test("accepts v-prefixed release tags", () => {
expect(parseSemver("v1.5.0")).toEqual({major: 1, minor: 5, patch: 0});
});

test("rejects extra numeric segments used on some git tags", () => {
expect(parseSemver("v1.4.8.1")).toBeNull();
});
});

describe("resolvePublishVersion", () => {
const published = ["1.4.6", "1.4.8"];

test("uses an unpublished GitHub release tag instead of package.json", () => {
expect(resolvePublishVersion("1.4.8", "v1.5.0", published)).toBe("1.5.0");
});

test("autoincrements patch when the release tag is already published", () => {
expect(resolvePublishVersion("1.4.8", "v1.4.8", published)).toBe("1.4.9");
});

test("falls back to package.json when the tag is not valid semver", () => {
expect(resolvePublishVersion("1.4.8", "v1.4.8.1", published)).toBe("1.4.9");
});

test("keeps walking patch until the version is unpublished", () => {
expect(resolvePublishVersion("1.4.8", "v1.4.8", ["1.4.8", "1.4.9"])).toBe("1.4.10");
});

test("uses package.json when it is already unpublished and no tag is set", () => {
expect(resolvePublishVersion("1.5.0", "", published)).toBe("1.5.0");
});

test("accepts a single published version string from npm view", () => {
expect(resolvePublishVersion("1.0.0", "v1.0.0", "1.0.0")).toBe("1.0.1");
});
});
Loading