diff --git a/.github/workflows/npm-release.yaml b/.github/workflows/npm-release.yaml index 89b69a6..ef63830 100644 --- a/.github/workflows/npm-release.yaml +++ b/.github/workflows/npm-release.yaml @@ -6,6 +6,9 @@ on: jobs: release: if: ${{ github.ref == 'refs/heads/main' && !startsWith(github.event.head_commit.message, 'NPM Package Release') }} + concurrency: + group: sqlite-web-npm-release + cancel-in-progress: false runs-on: ubuntu-latest permissions: id-token: write @@ -70,14 +73,14 @@ jobs: - name: Get Old Hash run: | OLD_HASH=$(npm view @rainlanguage/sqlite-web dist.shasum 2>/dev/null || echo "none") - echo "OLD_HASH=$OLD_HASH" >> $GITHUB_ENV + echo "OLD_HASH=$OLD_HASH" >> "$GITHUB_ENV" echo "old hash: $OLD_HASH" # Build the package and calc hash - name: Build and Get New Hash run: | nix develop -c local-bundle NEW_HASH=$(cd pkg && npm pack --silent | xargs shasum | cut -c1-40) - echo "NEW_HASH=$NEW_HASH" >> $GITHUB_ENV + echo "NEW_HASH=$NEW_HASH" >> "$GITHUB_ENV" echo "new hash: $NEW_HASH" rm -f pkg/*.tgz # from here on, we'll skip if OLD_HASH and NEW_HASH are the same (ie no publish) @@ -86,9 +89,10 @@ jobs: - name: Set Version if: ${{ env.OLD_HASH != env.NEW_HASH }} run: | - NEW_NPM_VERSION=$(npm --prefix pkg version patch --no-git-tag-version) - NEW_VERSION=${NEW_NPM_VERSION#v} - echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + PUBLISHED_VERSIONS=$(npm view @rainlanguage/sqlite-web versions --json) + NEW_VERSION=$(node scripts/next-release-version.mjs "$PUBLISHED_VERSIONS") + npm --prefix pkg version "$NEW_VERSION" --no-git-tag-version + echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_ENV" for manifest in packages/sqlite-web/Cargo.toml packages/sqlite-web-core/Cargo.toml; do sed -i.bak "s/^version = \".*\"/version = \"$NEW_VERSION\"/" "$manifest" rm "$manifest".bak @@ -99,7 +103,7 @@ jobs: if: ${{ env.OLD_HASH != env.NEW_HASH }} run: | cd pkg - echo "NPM_PACKAGE=$(npm pack --silent)" >> $GITHUB_ENV + echo "NPM_PACKAGE=$(npm pack --silent)" >> "$GITHUB_ENV" - name: Rename sqlite-web NPM Package Tarball if: ${{ env.OLD_HASH != env.NEW_HASH }} run: mv pkg/${{ env.NPM_PACKAGE }} sqlite_web_npm_package_${{ env.NEW_VERSION }}.tgz diff --git a/.github/workflows/test-wasm.yaml b/.github/workflows/test-wasm.yaml index af087e7..3025bdf 100644 --- a/.github/workflows/test-wasm.yaml +++ b/.github/workflows/test-wasm.yaml @@ -1,10 +1,8 @@ name: Test WASM on: [push] - concurrency: group: ${{ github.ref }}-wasm cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - jobs: test: permissions: @@ -18,7 +16,6 @@ jobs: with: submodules: recursive fetch-depth: 0 - - uses: nixbuild/nix-quick-install-action@v30 with: nix_conf: | @@ -35,11 +32,9 @@ jobs: # before trying to save a new cache # 1G = 1073741824 gc-max-store-size-linux: 1G - + - name: Test release version selection + run: nix develop -c node --test scripts/next-release-version.test.mjs - run: nix develop -c build-submodules - - run: nix develop -c local-bundle - - run: nix develop -c rainix-rs-static - - - run: nix develop -c test-wasm \ No newline at end of file + - run: nix develop -c test-wasm diff --git a/scripts/next-release-version.mjs b/scripts/next-release-version.mjs new file mode 100644 index 0000000..3737a0d --- /dev/null +++ b/scripts/next-release-version.mjs @@ -0,0 +1,49 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +function stableVersionParts(version) { + if (typeof version !== "string") { + throw new Error("Package versions must be strings."); + } + if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version)) { + return null; + } + return version.split(".").map(BigInt); +} + +// Release stable patches above every published stable version, including ones +// whose release commit failed or whose npm dist-tag is not `latest`. +export function nextReleaseVersion(localVersion, publishedVersions) { + let base = stableVersionParts(localVersion); + if (!base) { + throw new Error("The local package must have a stable release version."); + } + const versions = typeof publishedVersions === "string" + ? [publishedVersions] + : publishedVersions; + if (!Array.isArray(versions)) { + throw new Error("Expected npm's published version list."); + } + for (const version of versions) { + const parts = stableVersionParts(version); + if (!parts) continue; + for (let index = 0; index < 3; index++) { + if (parts[index] > base[index]) { + base = parts; + break; + } + if (parts[index] < base[index]) break; + } + } + return `${base[0]}.${base[1]}.${base[2] + 1n}`; +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(resolve(process.argv[1])).href +) { + const { version } = JSON.parse(readFileSync("pkg/package.json", "utf8")); + const publishedVersions = JSON.parse(process.argv[2]); + console.log(nextReleaseVersion(version, publishedVersions)); +} diff --git a/scripts/next-release-version.test.mjs b/scripts/next-release-version.test.mjs new file mode 100644 index 0000000..40d357b --- /dev/null +++ b/scripts/next-release-version.test.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { nextReleaseVersion } from "./next-release-version.mjs"; + +test("recovers from a published version whose release commit failed", () => { + assert.equal( + nextReleaseVersion("0.0.2", ["0.0.1", "0.0.2", "0.0.3"]), + "0.0.4", + ); +}); + +test("continues normal patches when repository and npm agree", () => { + assert.equal(nextReleaseVersion("0.0.4", ["0.0.3", "0.0.4"]), "0.0.5"); +}); + +test("preserves a higher local release line", () => { + assert.equal(nextReleaseVersion("1.0.0", ["0.0.3"]), "1.0.1"); +}); + +test("compares numeric versions across patch, minor, and major boundaries", () => { + assert.equal(nextReleaseVersion("0.0.2", ["0.0.9", "0.0.10"]), "0.0.11"); + assert.equal(nextReleaseVersion("0.9.9", ["0.10.0", "0.8.99"]), "0.10.1"); + assert.equal(nextReleaseVersion("0.99.99", ["1.0.0"]), "1.0.1"); +}); + +test("includes versions published under another dist-tag", () => { + assert.equal(nextReleaseVersion("0.0.2", ["0.0.4", "0.0.3"]), "0.0.5"); +}); + +test("ignores prerelease versions when selecting the stable release line", () => { + assert.equal( + nextReleaseVersion("0.0.2", ["0.0.3", "1.0.0-alpha.1"]), + "0.0.4", + ); +}); + +test("accepts npm's single-version response and an empty version list", () => { + assert.equal(nextReleaseVersion("0.0.2", "0.0.3"), "0.0.4"); + assert.equal(nextReleaseVersion("0.0.2", []), "0.0.3"); +}); + +test("rejects invalid local versions and registry response shapes", () => { + assert.throws(() => nextReleaseVersion("broken", []), /stable release/); + assert.throws(() => nextReleaseVersion("0.0.2", null), /version list/); + assert.throws(() => nextReleaseVersion("0.0.2", [null]), /must be strings/); +});