diff --git a/.github/workflows/nightly-schedule.yml b/.github/workflows/nightly-schedule.yml index 1766747d6..c0eb26238 100644 --- a/.github/workflows/nightly-schedule.yml +++ b/.github/workflows/nightly-schedule.yml @@ -9,6 +9,7 @@ jobs: permissions: packages: write contents: write + id-token: write uses: ./.github/workflows/release.yml secrets: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 376090922..f9fd28ec9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,6 +46,7 @@ jobs: permissions: packages: write contents: write + id-token: write outputs: api_image: ${{steps.set_image.outputs.api_image}} migration_image: ${{steps.migration-publish.outputs.image}} @@ -66,6 +67,13 @@ jobs: distribution: 'temurin' java-version: '11' cache: 'gradle' + - name: setup node + uses: actions/setup-node@v5.0.0 + with: + node-version: '24' + registry-url: 'https://registry.npmjs.org' + - name: setup npm + run: npm install --global npm@11.11.0 - name: Set version id: version env: @@ -93,7 +101,27 @@ jobs: - name: show version run: echo ${{ steps.version.outputs.version }} - name: build war - run: ./gradlew build --info --init-script init.gradle -PversionOverride=$VERSION + env: + VERSION: ${{ steps.version.outputs.version }} + run: ./gradlew build --info --init-script init.gradle "-PversionOverride=${VERSION}" + - name: Validate cwmsjs package + id: cwmsjs + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && steps.version.outputs.prerelease == 'false' + working-directory: clients/typescript/cwmsjs + run: | + PACKAGE_VERSION=$(node -p "require('./package.json').version") + echo "version=${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT" + + if VIEW_OUTPUT=$(npm view "cwmsjs@${PACKAGE_VERSION}" version 2>&1); then + echo "cwmsjs ${PACKAGE_VERSION} is already published; a retry will skip npm publish." + echo "published=true" >> "$GITHUB_OUTPUT" + elif grep -q "E404" <<< "${VIEW_OUTPUT}"; then + echo "published=false" >> "$GITHUB_OUTPUT" + npm publish --dry-run --access public --tag latest + else + echo "${VIEW_OUTPUT}" >&2 + exit 1 + fi - name: Create GitHub Release id: create_release # Allow testing without creating a release @@ -105,6 +133,10 @@ jobs: generate_release_notes: true prerelease: ${{steps.version.outputs.prerelease}} token: ${{ secrets.token != null && secrets.token || secrets.GITHUB_TOKEN }} + - name: Publish cwmsjs to npm + if: steps.create_release.outcome == 'success' && steps.cwmsjs.outputs.published == 'false' + working-directory: clients/typescript/cwmsjs + run: npm publish --access public --tag latest - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4.1.0 - name: Docker meta diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml index 53f93cda3..7ea4bd1e1 100644 --- a/.github/workflows/tagged-release.yml +++ b/.github/workflows/tagged-release.yml @@ -10,6 +10,10 @@ on: - '[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9]' jobs: release: + permissions: + contents: write + id-token: write + packages: write uses: ./.github/workflows/release.yml secrets: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/clients/typescript/README.md b/clients/typescript/README.md index 8321cc20a..9d51a1ee9 100644 --- a/clients/typescript/README.md +++ b/clients/typescript/README.md @@ -68,6 +68,7 @@ In order to accommodate changes both to the generator and to CDA itself, cwmsjs `[cwmsjs generator SemVer]-[CDA version/calver suffix]` The Gradle build passes the CDA project version into the client package step. When running the package update script directly, set `CDA_CLIENT_VERSION_SUFFIX` or pass `--version-suffix=`. +Zero-padded numeric CalVer identifiers are normalized for npm SemVer compatibility, so CDA release `2026.08.25` produces cwmsjs version `2.4.0-2026.8.25`. ## Building CWMSjs from source diff --git a/clients/typescript/build.gradle b/clients/typescript/build.gradle index 245340aa6..c4b95d493 100644 --- a/clients/typescript/build.gradle +++ b/clients/typescript/build.gradle @@ -104,6 +104,15 @@ tasks.register('modPackage', NpxTask) { outputs.file generatedClientDir.file('package.json') } +tasks.register('testPackageMetadata', NpmTask) { + group 'verification' + description 'Test cwmsjs package metadata and version generation.' + dependsOn npmInstall + npmCommand = ['run', 'test:package'] + inputs.files 'package.json', 'scripts/package-updates/modPackage.js', + 'scripts/package-updates/modPackage.test.js' +} + tasks.register('postGenerate', NpxTask) { group 'openapi' description 'Apply source patches required after OpenAPI generation.' @@ -155,3 +164,7 @@ tasks.named('build') { dependsOn buildTypeScriptClient dependsOn buildTypeScriptDocs } + +tasks.named('check') { + dependsOn testPackageMetadata +} diff --git a/clients/typescript/package.json b/clients/typescript/package.json index 83feebf0b..8a5df0fb8 100644 --- a/clients/typescript/package.json +++ b/clients/typescript/package.json @@ -41,6 +41,7 @@ "modPackage": "node ./scripts/package-updates/modPackage.js", "modSpec": "node ./scripts/spec-updates/modSpec.js", "postGenerate": "node ./scripts/postGenerate.js", + "test:package": "node --test ./scripts/package-updates/modPackage.test.js", "link": "cd cwmsjs && npm link && cd ../tests && npm link cwmsjs" }, "dependencies": { diff --git a/clients/typescript/scripts/package-updates/modPackage.js b/clients/typescript/scripts/package-updates/modPackage.js index 32ac03fcb..9346d3378 100644 --- a/clients/typescript/scripts/package-updates/modPackage.js +++ b/clients/typescript/scripts/package-updates/modPackage.js @@ -15,19 +15,39 @@ function writeJson(relativePath, value) { ); } -function getVersionSuffixArg() { +function getVersionSuffixArg(argv = process.argv.slice(2)) { const prefix = "--version-suffix="; - const value = process.argv.find((arg) => arg.startsWith(prefix)); - return value ? value.slice(prefix.length) : process.argv[2]; + const value = argv.find((arg) => arg.startsWith(prefix)); + return value ? value.slice(prefix.length) : argv[0]; } -function getVersionSuffix() { +function normalizeVersionSuffix(value) { + if (typeof value !== "string" || value.trim() === "") { + throw new Error("Version suffix must be a non-empty string."); + } + + return value + .trim() + .split(".") + .map((identifier) => { + if (!/^[0-9A-Za-z-]+$/.test(identifier)) { + throw new Error(`Invalid version suffix identifier: ${identifier}`); + } + + return /^\d+$/.test(identifier) + ? BigInt(identifier).toString() + : identifier; + }) + .join("."); +} + +function getVersionSuffix(argv = process.argv.slice(2), env = process.env) { const explicitVersion = - getVersionSuffixArg() || - process.env.CDA_CLIENT_VERSION_SUFFIX || - process.env.CWMSJS_VERSION_SUFFIX; + getVersionSuffixArg(argv) || + env.CDA_CLIENT_VERSION_SUFFIX || + env.CWMSJS_VERSION_SUFFIX; if (explicitVersion) { - return explicitVersion; + return normalizeVersionSuffix(explicitVersion); } throw new Error( @@ -60,4 +80,11 @@ function main() { ); } -main(); +if (require.main === module) { + main(); +} + +module.exports = { + getVersionSuffix, + normalizeVersionSuffix, +}; diff --git a/clients/typescript/scripts/package-updates/modPackage.test.js b/clients/typescript/scripts/package-updates/modPackage.test.js new file mode 100644 index 000000000..026b2241b --- /dev/null +++ b/clients/typescript/scripts/package-updates/modPackage.test.js @@ -0,0 +1,50 @@ +const assert = require("node:assert/strict"); +const test = require("node:test"); + +const { getVersionSuffix, normalizeVersionSuffix } = require("./modPackage.js"); + +test("normalizes zero-padded production CalVer identifiers", () => { + assert.equal(normalizeVersionSuffix("2026.08.25"), "2026.8.25"); +}); + +test("preserves a suffixed production release", () => { + assert.equal(normalizeVersionSuffix("2026.03.31-b"), "2026.3.31-b"); +}); + +test("normalizes prerelease and nightly version suffixes", () => { + assert.equal(normalizeVersionSuffix("2026.08.25-dev"), "2026.8.25-dev"); + assert.equal(normalizeVersionSuffix("develop-nightly"), "develop-nightly"); +}); + +test("rejects empty and invalid version suffixes", () => { + assert.throws(() => normalizeVersionSuffix(""), /non-empty string/); + assert.throws(() => normalizeVersionSuffix("2026..25"), /Invalid version/); + assert.throws( + () => normalizeVersionSuffix("release/2026.08.25"), + /Invalid version/, + ); +}); + +test("reads the CLI argument before environment variables", () => { + assert.equal( + getVersionSuffix(["--version-suffix=2026.08.25"], { + CDA_CLIENT_VERSION_SUFFIX: "2026.03.31", + }), + "2026.8.25", + ); +}); + +test("reads supported environment variables", () => { + assert.equal( + getVersionSuffix([], { CDA_CLIENT_VERSION_SUFFIX: "2026.08.25-test" }), + "2026.8.25-test", + ); + assert.equal( + getVersionSuffix([], { CWMSJS_VERSION_SUFFIX: "2026.08.25-dev" }), + "2026.8.25-dev", + ); +}); + +test("requires an explicit version suffix", () => { + assert.throws(() => getVersionSuffix([], {}), /Missing version suffix/); +}); diff --git a/clients/typescript/scripts/package-updates/updates.json b/clients/typescript/scripts/package-updates/updates.json index 43c5defcd..35ba30f30 100644 --- a/clients/typescript/scripts/package-updates/updates.json +++ b/clients/typescript/scripts/package-updates/updates.json @@ -1,16 +1,23 @@ { "description": "CWMS Data API Library for JavaScript/TypeScript created with OpenAPI generator for use with browser webapps", + "files": [ + "dist/", + "src/", + "tsconfig.json", + "tsconfig.esm.json" + ], "unpkg": "./dist/cwmsjs.min.js", "jsdelivr": "./dist/cwmsjs.min.js", "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/esm/index.js", + "import": "./dist/cwmsjs.mjs", "require": "./dist/index.js" } }, "scripts": { - "build": "tsc && tsc -p tsconfig.esm.json && npm run build:browser", + "build": "tsc && tsc -p tsconfig.esm.json && npm run build:esm && npm run build:browser", + "build:esm": "esbuild dist/esm/index.js --bundle --format=esm --platform=neutral --outfile=dist/cwmsjs.mjs", "build:browser": "esbuild dist/esm/index.js --bundle --format=iife --global-name=cwmsjs --outfile=dist/cwmsjs.js && esbuild dist/esm/index.js --bundle --format=iife --global-name=cwmsjs --minify --sourcemap --outfile=dist/cwmsjs.min.js", "prepare": "npm run build" }, diff --git a/clients/typescript/tests/smoke.js b/clients/typescript/tests/smoke.js index e380c31cb..62b515326 100644 --- a/clients/typescript/tests/smoke.js +++ b/clients/typescript/tests/smoke.js @@ -5,6 +5,12 @@ import { OfficesApi, ProjectsApi, } from "../cwmsjs/dist/index.js"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const { + normalizeVersionSuffix, +} = require("../scripts/package-updates/modPackage.js"); if (!global.fetch) { throw new Error( @@ -29,7 +35,7 @@ async function main() { let expectedVersion = process.env.EXPECTED_CWMSJS_VERSION; if (!expectedVersion && process.env.CDA_CLIENT_VERSION_SUFFIX) { - expectedVersion = `${rootPackageJson.version}-${process.env.CDA_CLIENT_VERSION_SUFFIX}`; + expectedVersion = `${rootPackageJson.version}-${normalizeVersionSuffix(process.env.CDA_CLIENT_VERSION_SUFFIX)}`; } if (!expectedVersion) { @@ -39,7 +45,7 @@ async function main() { "utf8", ), ); - expectedVersion = `${rootPackageJson.version}-${rawSpec?.info?.version}`; + expectedVersion = `${rootPackageJson.version}-${normalizeVersionSuffix(rawSpec?.info?.version)}`; } if (packageJson.version !== expectedVersion) {