Skip to content

Commit 945c86a

Browse files
authored
Merge pull request #19 from witqq/chore/coordinated-release-node24
chore: modernize Node and package releases
2 parents 9970f6e + 0ed0d78 commit 945c86a

52 files changed

Lines changed: 6398 additions & 6951 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: npm
4+
directory: /
5+
schedule:
6+
interval: monthly
7+
open-pull-requests-limit: 5
8+
- package-ecosystem: github-actions
9+
directory: /
10+
schedule:
11+
interval: monthly
12+
open-pull-requests-limit: 5

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- master
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: ci-${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
verify:
19+
name: Node.js 24 coordinated release gate
20+
runs-on: ubuntu-24.04
21+
timeout-minutes: 20
22+
steps:
23+
- name: Check out the repository
24+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25+
- name: Use Node.js 24
26+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
27+
with:
28+
node-version: 24.20.0
29+
package-manager-cache: false
30+
- name: Use npm 12
31+
run: npm install --global npm@12.0.2
32+
- name: Install locked dependencies
33+
run: npm ci --no-audit --no-fund
34+
- name: Verify dependencies, runtime and coordinated candidates
35+
run: npm run verify

.github/workflows/publish-npm.yml

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
name: Publish coordinated npm release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
tag:
7+
description: Annotated coordinated release tag, for example v0.6.0
8+
required: true
9+
type: string
10+
manifest_sha256:
11+
description: Accepted SHA-256 of spreadsheet-release-VERSION.json
12+
required: true
13+
type: string
14+
15+
permissions:
16+
contents: read
17+
id-token: write
18+
19+
concurrency:
20+
group: npm-publish-${{ inputs.tag }}
21+
cancel-in-progress: false
22+
23+
jobs:
24+
publish:
25+
name: Verify and publish six exact assets
26+
runs-on: ubuntu-24.04
27+
timeout-minutes: 15
28+
steps:
29+
- name: Use Node.js 24
30+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
31+
with:
32+
node-version: 24.20.0
33+
registry-url: https://registry.npmjs.org
34+
package-manager-cache: false
35+
36+
- name: Install an OIDC-capable npm CLI
37+
run: npm install --global npm@12.0.2
38+
39+
- name: Preflight and publish the coordinated release
40+
env:
41+
GH_TOKEN: ${{ github.token }}
42+
INPUT_MANIFEST_SHA256: ${{ inputs.manifest_sha256 }}
43+
INPUT_TAG: ${{ inputs.tag }}
44+
run: |
45+
set -euo pipefail
46+
47+
tag="${INPUT_TAG}"
48+
expected_manifest_sha256="${INPUT_MANIFEST_SHA256}"
49+
if [[ ! "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
50+
echo "tag must match vMAJOR.MINOR.PATCH" >&2
51+
exit 1
52+
fi
53+
if [[ ! "${expected_manifest_sha256}" =~ ^[0-9a-f]{64}$ ]]; then
54+
echo "manifest_sha256 must contain 64 lowercase hexadecimal characters" >&2
55+
exit 1
56+
fi
57+
58+
version="${tag#v}"
59+
manifest_name="spreadsheet-release-${version}.json"
60+
release_json="${RUNNER_TEMP}/release.json"
61+
ref_json="${RUNNER_TEMP}/tag-ref.json"
62+
tag_json="${RUNNER_TEMP}/annotated-tag.json"
63+
compare_json="${RUNNER_TEMP}/compare.json"
64+
manifest="${RUNNER_TEMP}/${manifest_name}"
65+
package_rows="${RUNNER_TEMP}/packages.tsv"
66+
67+
gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}" > "${ref_json}"
68+
# shellcheck disable=SC2016
69+
annotated_tag_sha="$(node -e '
70+
const fs = require("node:fs");
71+
const ref = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
72+
if (ref.object?.type !== "tag" || !/^[0-9a-f]{40}$/.test(ref.object?.sha)) throw new Error("release tag must be annotated");
73+
process.stdout.write(ref.object.sha);
74+
' "${ref_json}")"
75+
gh api "repos/${GITHUB_REPOSITORY}/git/tags/${annotated_tag_sha}" > "${tag_json}"
76+
# shellcheck disable=SC2016
77+
tag_commit="$(node -e '
78+
const fs = require("node:fs");
79+
const [file, expectedTag] = process.argv.slice(1);
80+
const tagObject = JSON.parse(fs.readFileSync(file, "utf8"));
81+
if (tagObject.tag !== expectedTag || tagObject.object?.type !== "commit" || !/^[0-9a-f]{40}$/.test(tagObject.object?.sha)) throw new Error("annotated tag identity is invalid");
82+
process.stdout.write(tagObject.object.sha);
83+
' "${tag_json}" "${tag}")"
84+
gh api "repos/${GITHUB_REPOSITORY}/compare/${tag_commit}...${GITHUB_SHA}" > "${compare_json}"
85+
# shellcheck disable=SC2016
86+
node -e '
87+
const fs = require("node:fs");
88+
const [file, expectedBase] = process.argv.slice(1);
89+
const comparison = JSON.parse(fs.readFileSync(file, "utf8"));
90+
if (!["ahead", "identical"].includes(comparison.status) || comparison.merge_base_commit?.sha !== expectedBase) throw new Error("tag commit is not contained in dispatched master");
91+
' "${compare_json}" "${tag_commit}"
92+
93+
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" > "${release_json}"
94+
# shellcheck disable=SC2016
95+
manifest_url="$(node -e '
96+
const fs = require("node:fs");
97+
const [file, expectedTag, expectedName, expectedDigest] = process.argv.slice(1);
98+
const release = JSON.parse(fs.readFileSync(file, "utf8"));
99+
if (release.tag_name !== expectedTag || release.draft || release.prerelease || release.assets?.length !== 7) throw new Error("release state or asset count is invalid");
100+
const asset = release.assets.find(candidate => candidate.name === expectedName);
101+
if (!asset || asset.state !== "uploaded" || asset.digest !== `sha256:${expectedDigest}` || asset.size <= 0) throw new Error("release manifest asset is invalid");
102+
process.stdout.write(asset.browser_download_url);
103+
' "${release_json}" "${tag}" "${manifest_name}" "${expected_manifest_sha256}")"
104+
curl --fail --location --proto '=https' --tlsv1.2 --output "${manifest}" "${manifest_url}"
105+
actual_manifest_sha256="$(shasum -a 256 "${manifest}" | cut -d ' ' -f 1)"
106+
test "${actual_manifest_sha256}" = "${expected_manifest_sha256}"
107+
108+
# shellcheck disable=SC2016
109+
node -e '
110+
const fs = require("node:fs");
111+
const [manifestFile, releaseFile, rowsFile, expectedTag, expectedVersion, expectedRevision] = process.argv.slice(1);
112+
const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
113+
const release = JSON.parse(fs.readFileSync(releaseFile, "utf8"));
114+
const names = ["@witqq/spreadsheet", "@witqq/spreadsheet-plugins", "@witqq/spreadsheet-react", "@witqq/spreadsheet-vue", "@witqq/spreadsheet-angular", "@witqq/spreadsheet-widget"];
115+
if (manifest.schemaVersion !== 1 || manifest.tag !== expectedTag || manifest.version !== expectedVersion || manifest.sourceRevision !== expectedRevision) throw new Error("coordinated manifest identity is invalid");
116+
if (JSON.stringify(manifest.publishOrder) !== JSON.stringify(names) || manifest.packages?.length !== names.length) throw new Error("publish order or package count is invalid");
117+
const releaseAssets = new Map(release.assets.map(asset => [asset.name, asset]));
118+
const rows = manifest.packages.map((item, index) => {
119+
const expectedAsset = `${names[index].slice(1).replace("/", "-")}-${expectedVersion}.tgz`;
120+
if (item.name !== names[index] || item.version !== undefined || item.asset !== expectedAsset || item.repositoryDirectory !== item.workspace || !/^packages\/(core|plugins|react|vue|angular|widget)$/.test(item.workspace) || !/^[0-9a-f]{64}$/.test(item.sha256) || !Number.isSafeInteger(item.size) || item.size <= 0) throw new Error(`manifest package ${index} is invalid`);
121+
const asset = releaseAssets.get(item.asset);
122+
if (!asset || asset.state !== "uploaded" || asset.digest !== `sha256:${item.sha256}` || asset.size !== item.size) throw new Error(`release asset ${item.asset} is invalid`);
123+
return [index, item.name, item.workspace, item.asset, item.sha256, asset.browser_download_url].join("\t");
124+
});
125+
const expectedAssets = new Set([`spreadsheet-release-${expectedVersion}.json`, ...manifest.packages.map(item => item.asset)]);
126+
if (releaseAssets.size !== expectedAssets.size || [...releaseAssets.keys()].some(name => !expectedAssets.has(name))) throw new Error("release contains an unexpected asset");
127+
fs.writeFileSync(rowsFile, `${rows.join("\n")}\n`);
128+
' "${manifest}" "${release_json}" "${package_rows}" "${tag}" "${version}" "${tag_commit}"
129+
130+
while IFS=$'\t' read -r index package_name workspace asset_name expected_sha256 asset_url; do
131+
tarball="${RUNNER_TEMP}/${asset_name}"
132+
package_manifest="${RUNNER_TEMP}/package-${index}.json"
133+
curl --fail --location --proto '=https' --tlsv1.2 --output "${tarball}" "${asset_url}"
134+
actual_sha256="$(shasum -a 256 "${tarball}" | cut -d ' ' -f 1)"
135+
test "${actual_sha256}" = "${expected_sha256}"
136+
tar -xOf "${tarball}" package/package.json > "${package_manifest}"
137+
# shellcheck disable=SC2016
138+
node -e '
139+
const fs = require("node:fs");
140+
const [file, expectedName, expectedVersion, expectedWorkspace] = process.argv.slice(1);
141+
const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
142+
if (manifest.name !== expectedName || manifest.version !== expectedVersion || manifest.repository?.url !== "https://github.com/witqq/spreadsheet.git" || manifest.repository?.directory !== expectedWorkspace || manifest.engines?.node !== ">=24.20.0" || manifest.publishConfig?.access !== "public") throw new Error("package identity is invalid");
143+
if (expectedName !== "@witqq/spreadsheet" && manifest.dependencies?.["@witqq/spreadsheet"] !== `^${expectedVersion}`) throw new Error("internal dependency version is invalid");
144+
' "${package_manifest}" "${package_name}" "${version}" "${workspace}"
145+
done < "${package_rows}"
146+
147+
while IFS=$'\t' read -r index package_name workspace asset_name expected_sha256 asset_url; do
148+
existing_url="$(npm view "${package_name}@${version}" dist.tarball --json 2>/dev/null || true)"
149+
if [[ -n "${existing_url}" && "${existing_url}" != "null" ]]; then
150+
registry_url="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]))' "${existing_url}")"
151+
registry_tarball="${RUNNER_TEMP}/registry-preflight-${index}.tgz"
152+
curl --fail --location --proto '=https' --tlsv1.2 --output "${registry_tarball}" "${registry_url}"
153+
registry_sha256="$(shasum -a 256 "${registry_tarball}" | cut -d ' ' -f 1)"
154+
test "${registry_sha256}" = "${expected_sha256}"
155+
fi
156+
done < "${package_rows}"
157+
158+
while IFS=$'\t' read -r index package_name workspace asset_name expected_sha256 asset_url; do
159+
if npm view "${package_name}@${version}" version --json >/dev/null 2>&1; then
160+
echo "${package_name}@${version} already contains the accepted bytes; skipping"
161+
else
162+
npm publish --access public "${asset_url}"
163+
fi
164+
done < "${package_rows}"
165+
166+
while IFS=$'\t' read -r index package_name workspace asset_name expected_sha256 asset_url; do
167+
registry_url="$(npm view "${package_name}@${version}" dist.tarball --json | node -e 'let value=""; process.stdin.on("data", chunk => value += chunk); process.stdin.on("end", () => process.stdout.write(JSON.parse(value)));')"
168+
registry_tarball="${RUNNER_TEMP}/registry-final-${index}.tgz"
169+
curl --fail --location --proto '=https' --tlsv1.2 --output "${registry_tarball}" "${registry_url}"
170+
registry_sha256="$(shasum -a 256 "${registry_tarball}" | cut -d ' ' -f 1)"
171+
test "${registry_sha256}" = "${expected_sha256}"
172+
done < "${package_rows}"

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,9 @@ moira-ws/
2525
# Claude supervisor logs
2626
.supervisor_logs/
2727
.idea/
28-
docker-compose.remote.yml
2928
CLAUDE.md
3029

3130
# Internal files (not for public repo)
3231
research/
3332
deploy-logs/
34-
.github/workflows/
3533
tmp/

.node-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24.20.0

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24.20.0

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.6.0] — 2026-09-04
11+
12+
### Infrastructure
13+
14+
- Require Node.js 24.20.0 across the monorepo and all public package contracts.
15+
- Update direct and transitive dependencies, add pinned CI and coordinated immutable GitHub Release asset publication for all six public packages through npm OpenID Connect trusted publishing.
16+
1017
### Added
1118

1219
- `DecoratorsPlugin`: built-in plugin bundling six reusable cell decorators — TreeExpander, SortIcon, ProgressBar, Link, Image, and Spinner. Configurable via `DecoratorsPluginConfig` (enable/disable individual decorators)
@@ -76,7 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7683

7784
### Changed
7885

79-
- `@witqq/spreadsheet-react`: peerDependencies now include `react: ^17.0.0` and `react-dom: ^17.0.0`. The wrapper uses only React ≤16-era hooks (`useEffect`, `useRef`, `useImperativeHandle`, `forwardRef`) and the automatic JSX transform (`react-jsx`) available since React 17.0.0
86+
- `@witqq/spreadsheet-react`: peer dependencies support React and React DOM 17, 18, and 19. The wrapper uses only React ≤16-era hooks (`useEffect`, `useRef`, `useImperativeHandle`, `forwardRef`) and the automatic JSX transform (`react-jsx`) available since React 17.0.0
8087
- `DatePickerEditor`: refactored from thin adapter (delegating to `DatePickerOverlay`) to direct `BaseOverlayEditor` subclass. Public API unchanged
8188
- `DateTimeEditor`: refactored from monolithic 821-line class to ~300-line `BaseOverlayEditor` subclass. Public API unchanged
8289
- `DatePickerOverlay`: internal utility functions replaced with imports from `calendar-utils` module. Public API unchanged

CONTRIBUTING.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# Contributing to witqq spreadsheet
22

3-
Thank you for your interest in contributing! For full documentation, interactive demos, and API reference, visit the [project website](https://witqq.dev).
3+
Thank you for your interest in contributing! For full documentation, interactive demos, and API reference, visit the [project website](https://spreadsheet.witqq.dev).
44

55
## Getting Started
66

7+
Node.js 24.20.0 or newer and npm 12.0.2 are required for development.
8+
79
1. Fork the repository
8-
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/wit-table.git`
10+
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/spreadsheet.git`
911
3. Install dependencies: `npm install`
1012
4. Start the development server: `npm run dev`
1113

@@ -37,7 +39,7 @@ npm run docs:npm # Generate npm package docs from site MDX
3739

3840
1. Create a feature branch from `master`
3941
2. Make your changes with tests
40-
3. Run `npm run build && npm run test` to verify
42+
3. Run `npm run verify` to execute the complete release gate
4143
4. Submit a pull request
4244

4345
## Code Style

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Canvas-based spreadsheet and datagrid engine for React, Vue, Angular, and vanilla JS. Zero external dependencies in the core package.
44

5+
[![CI](https://github.com/witqq/spreadsheet/actions/workflows/ci.yml/badge.svg)](https://github.com/witqq/spreadsheet/actions/workflows/ci.yml)
6+
57
```bash
68
npm install @witqq/spreadsheet @witqq/spreadsheet-react
79
```
@@ -96,6 +98,8 @@ Key areas:
9698

9799
## Development
98100

101+
Node.js 24.20.0 or newer is required. Development, CI and release checks use npm 12.0.2.
102+
99103
```bash
100104
npm install # Install dependencies
101105
npm run build # Build all packages
@@ -106,6 +110,8 @@ npm run lint # ESLint
106110
npm run dev # Docker dev server on port 3150
107111
```
108112

113+
Maintainers use the coordinated immutable-asset process in [`docs/RELEASE.md`](docs/RELEASE.md) to release all six public packages together.
114+
109115
## License
110116

111117
[BSL 1.1](LICENSE) — Free for non-commercial use. Commercial use requires a paid license. Change Date: 2030-03-01 → Apache License 2.0.

config/Dockerfile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# syntax=docker/dockerfile:1
22

3-
FROM node:20 AS deps
3+
FROM node:24.20.0-bookworm AS deps
4+
RUN npm install --global npm@12.0.2
45
WORKDIR /app
56
COPY package*.json ./
67
COPY packages/core/package.json packages/core/
78
COPY packages/react/package.json packages/react/
9+
COPY packages/vue/package.json packages/vue/
10+
COPY packages/angular/package.json packages/angular/
11+
COPY packages/widget/package.json packages/widget/
812
COPY packages/plugins/package.json packages/plugins/
913
COPY packages/server/package.json packages/server/
1014
COPY packages/demo/package.json packages/demo/
@@ -21,13 +25,16 @@ RUN --mount=type=cache,target=/root/.npm \
2125
FROM deps AS build
2226
COPY packages/core/ packages/core/
2327
COPY packages/react/ packages/react/
28+
COPY packages/vue/ packages/vue/
29+
COPY packages/angular/ packages/angular/
30+
COPY packages/widget/ packages/widget/
2431
COPY packages/plugins/ packages/plugins/
2532
COPY packages/server/ packages/server/
2633
COPY packages/demo/ packages/demo/
2734
COPY packages/site/ packages/site/
2835
COPY tsconfig*.json ./
2936
ARG BUILD_TIME=unknown
30-
RUN npm run build && npm run build -w packages/demo && npm run build -w packages/site
37+
RUN npm run build
3138

3239
FROM nginx:alpine
3340
ARG BUILD_TIME=unknown

0 commit comments

Comments
 (0)