@williamthorsen/release-kit 8.0.1 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +40 -4
- package/dist/esm/buildChangelogEntries.js +2 -2
- package/dist/esm/changelogOverrides.js +4 -3
- package/dist/esm/createGithubRelease.js +2 -2
- package/dist/esm/detectPackageManager.js +1 -1
- package/dist/esm/format.js +2 -2
- package/dist/esm/getCommitsSinceTarget.js +1 -1
- package/dist/esm/injectReleaseNotesIntoReadme.js +1 -1
- package/dist/esm/prepareCommand.js +7 -6
- package/dist/esm/previewTagPrefixes.js +2 -5
- package/dist/esm/releasePrepareMono.js +6 -5
- package/dist/esm/renderReleaseNotes.js +2 -2
- package/dist/esm/resolveCommandTags.js +4 -3
- package/dist/esm/sync-labels/generateCommand.js +1 -1
- package/dist/esm/sync-labels/templates.js +27 -3
- package/dist/esm/writeReleaseNotesPreviews.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 8.1.0 — 2026-07-22
|
|
6
|
+
|
|
7
|
+
### 🎉 Features
|
|
8
|
+
|
|
9
|
+
- Make label deletions reviewable and apply labels on merge (#493)
|
|
10
|
+
|
|
11
|
+
Pull requests that touch a repository's label file now get a check listing every label change the sync would make, including deletions of labels the file never mentions, which a diff of the file cannot show. Merging the pull request now applies those labels. The sync's deletion and rename handling now changes only through a reviewed upgrade to a verified release. Repositories that already scaffolded this workflow adopt the new triggers by re-running `release-kit sync-labels init --force`.
|
|
12
|
+
|
|
13
|
+
### 📦 Dependencies
|
|
14
|
+
|
|
15
|
+
- Upgrade dependencies and align the Node support policy (#483)
|
|
16
|
+
|
|
17
|
+
All four published packages (`nmr`, `nmr-core`, `release-kit`, and `v11y-check`) now require Node.js 24 or later, up from Node 18.17. Separately, `nmr-compile` now rebuilds when the TypeScript version changes.
|
|
18
|
+
|
|
5
19
|
## 8.0.1 — 2026-07-18
|
|
6
20
|
|
|
7
21
|
### 🐛 Bug fixes
|
package/README.md
CHANGED
|
@@ -5,17 +5,19 @@ Version-bumping and changelog-generation toolkit for release workflows.
|
|
|
5
5
|
Provides a self-contained CLI that auto-discovers workspaces from `pnpm-workspace.yaml`, parses conventional commits, determines version bumps, updates `package.json` files, and generates changelogs from `git-cliff --context` output rendered in-process (with optional [editorial overrides](#editorial-overrides)).
|
|
6
6
|
|
|
7
7
|
<!-- section:release-notes -->
|
|
8
|
-
## Release notes — v8.0
|
|
8
|
+
## Release notes — v8.1.0 (2026-07-22)
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### 🎉 Features
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- Make label deletions reviewable and apply labels on merge (#493)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Pull requests that touch a repository's label file now get a check listing every label change the sync would make, including deletions of labels the file never mentions, which a diff of the file cannot show. Merging the pull request now applies those labels. The sync's deletion and rename handling now changes only through a reviewed upgrade to a verified release. Repositories that already scaffolded this workflow adopt the new triggers by re-running `release-kit sync-labels init --force`.
|
|
15
15
|
<!-- /section:release-notes -->
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
19
|
+
Requires Node.js 24 or later.
|
|
20
|
+
|
|
19
21
|
```bash
|
|
20
22
|
pnpm add -D @williamthorsen/release-kit
|
|
21
23
|
```
|
|
@@ -728,6 +730,40 @@ Manage GitHub label definitions via config-driven YAML files.
|
|
|
728
730
|
|
|
729
731
|
`init` scaffolds `.config/sync-labels.config.ts` with auto-detected workspace scope labels and a `.github/workflows/sync-labels.yaml` caller workflow, then generates `.github/labels.yaml`. `generate` reads the config and writes `.github/labels.yaml`. `sync` triggers the workflow remotely — it requires the `gh` CLI and an existing workflow file.
|
|
730
732
|
|
|
733
|
+
#### When labels are applied
|
|
734
|
+
|
|
735
|
+
The scaffolded workflow carries three triggers:
|
|
736
|
+
|
|
737
|
+
| Trigger | Job | Token | Effect |
|
|
738
|
+
| --------------------------------------------------------- | ------- | --------------- | --------------------------------- |
|
|
739
|
+
| Push to the default branch touching `.github/labels.yaml` | `sync` | `issues: write` | Applies the labels |
|
|
740
|
+
| Manual dispatch (`release-kit sync-labels sync`) | `sync` | `issues: write` | Applies the labels |
|
|
741
|
+
| Pull request touching `.github/labels.yaml` | `check` | `issues: read` | Logs the diff without applying it |
|
|
742
|
+
|
|
743
|
+
Applying on merge closes the window in which a regenerated `.github/labels.yaml` sits unapplied — a window in which a later manual dispatch would apply a label set nobody reviewed.
|
|
744
|
+
|
|
745
|
+
The `check` job runs the same sync in dry-run. Its **Label diff** log group lists every create, edit, rename, and deletion the sync would perform, including deletions of labels the file does not declare — the destructive edits a diff of `.github/labels.yaml` alone cannot show, because a label created by hand or by another workflow is absent from the file both before and after. Read that log; the check reports success whether or not the diff is destructive, so a green check is not evidence that nothing will be deleted.
|
|
746
|
+
|
|
747
|
+
The two jobs are split because a job's permissions are fixed when the run is created and cannot vary by trigger. The split is what keeps a write-scoped token out of pull-request runs.
|
|
748
|
+
|
|
749
|
+
The push trigger filters on the path alone; the `sync` job compares `github.ref_name` against the repository's default branch. The workflow therefore needs no per-repo edit whatever that branch is named — and a push touching `.github/labels.yaml` on any other branch produces a run whose jobs all skip.
|
|
750
|
+
|
|
751
|
+
Manual dispatch is not a preview. It matches the `sync` job, so it applies the labels, deletions included; only the pull-request path runs in dry-run.
|
|
752
|
+
|
|
753
|
+
#### Dry-run checks on fork pull requests
|
|
754
|
+
|
|
755
|
+
GitHub issues a read-only `GITHUB_TOKEN` to pull requests from forks, and by default holds runs from first-time contributors until a maintainer approves them. The dry-run reads labels and writes nothing, so the check normally runs once approved. A repo that disables workflows on fork pull requests gets no check at all; to preview such a change, push the branch to the base repo and open the pull request from there.
|
|
756
|
+
|
|
757
|
+
#### Migrating an existing repo
|
|
758
|
+
|
|
759
|
+
The caller workflow gained the `push` and `pull_request` triggers after its first release. A repo scaffolded before that keeps working on manual dispatch alone — nothing breaks — but it applies labels only when someone remembers to. To adopt the new shape:
|
|
760
|
+
|
|
761
|
+
```sh
|
|
762
|
+
pnpm exec release-kit sync-labels init --force
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
`readyup` prompts for this: its `sync-labels.yaml matches template` check fails once the scaffolded workflow no longer matches the current template.
|
|
766
|
+
|
|
731
767
|
#### Published JSON Schema for `.meta/label-map.json`
|
|
732
768
|
|
|
733
769
|
release-kit publishes a JSON Schema for `.meta/label-map.json` — a separate, generic data file that maps commit-prefix scopes and types to GitHub label names. The schema lives at `packages/release-kit/schemas/label-map.json` in this repo and is reachable via the stable raw URL:
|
|
@@ -9,7 +9,7 @@ const HTML_COMMENT_PREFIX_PATTERN = /^<!--[^>]*-->/;
|
|
|
9
9
|
const CANONICAL_SECTION_ORDER = new Map(Object.values(DEFAULT_WORK_TYPES).map((config, index) => [stripGroupDecorations(config.header), index]));
|
|
10
10
|
function canonicalSectionPriority(title) {
|
|
11
11
|
const index = CANONICAL_SECTION_ORDER.get(stripGroupDecorations(title));
|
|
12
|
-
return index ??
|
|
12
|
+
return index ?? Infinity;
|
|
13
13
|
}
|
|
14
14
|
export function stripGroupDecorations(group) {
|
|
15
15
|
return stripEmojiPrefix(group.replace(HTML_COMMENT_PREFIX_PATTERN, ''));
|
|
@@ -132,7 +132,7 @@ function subjectHasBreakingMarker(message) {
|
|
|
132
132
|
return /^(?:[^|]+\|)?\w+(?:\([^)]+\))?!:/.test(subject);
|
|
133
133
|
}
|
|
134
134
|
function extractDescription(message) {
|
|
135
|
-
const firstLine = message.split('\n')[0] ?? message;
|
|
135
|
+
const firstLine = message.split('\n', 1)[0] ?? message;
|
|
136
136
|
const afterColon = firstLine.split(': ').slice(1).join(': ');
|
|
137
137
|
if (afterColon.length > 0) {
|
|
138
138
|
return afterColon.charAt(0).toUpperCase() + afterColon.slice(1);
|
|
@@ -59,10 +59,11 @@ function validateSingleOverride(key, rawEntry, errors) {
|
|
|
59
59
|
}
|
|
60
60
|
let entryValid = true;
|
|
61
61
|
for (const fieldName of Object.keys(rawEntry)) {
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
entryValid = false;
|
|
62
|
+
if (KNOWN_OVERRIDE_FIELDS.has(fieldName)) {
|
|
63
|
+
continue;
|
|
65
64
|
}
|
|
65
|
+
errors.push(`overrides['${key}']: unknown field '${fieldName}'`);
|
|
66
|
+
entryValid = false;
|
|
66
67
|
}
|
|
67
68
|
const result = {};
|
|
68
69
|
if (rawEntry.audience !== undefined) {
|
|
@@ -26,7 +26,7 @@ export function createGithubRelease(options) {
|
|
|
26
26
|
const body = renderReleaseNotesSingle(entry, {
|
|
27
27
|
filter: matchesAudience('all'),
|
|
28
28
|
includeHeading: false,
|
|
29
|
-
...(sectionOrder
|
|
29
|
+
...(sectionOrder !== undefined && { sectionOrder }),
|
|
30
30
|
});
|
|
31
31
|
if (body.trim() === '') {
|
|
32
32
|
return { status: 'skipped', reason: 'empty-body' };
|
|
@@ -48,7 +48,7 @@ export function createGithubReleases(tags, changelogJsonOutputPath, dryRun, sect
|
|
|
48
48
|
tag,
|
|
49
49
|
changelogJsonPath,
|
|
50
50
|
dryRun,
|
|
51
|
-
...(sectionOrder
|
|
51
|
+
...(sectionOrder !== undefined && { sectionOrder }),
|
|
52
52
|
});
|
|
53
53
|
if (result.status === 'created') {
|
|
54
54
|
created.push(tag);
|
|
@@ -7,7 +7,7 @@ export function detectPackageManager() {
|
|
|
7
7
|
const content = readFileSync(packageJsonPath, 'utf8');
|
|
8
8
|
const parsed = JSON.parse(content);
|
|
9
9
|
if (isRecord(parsed) && typeof parsed.packageManager === 'string') {
|
|
10
|
-
const [name, version] = parsed.packageManager.split('@');
|
|
10
|
+
const [name, version] = parsed.packageManager.split('@', 2);
|
|
11
11
|
if (name === 'pnpm' || name === 'npm') {
|
|
12
12
|
return name;
|
|
13
13
|
}
|
package/dist/esm/format.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function bold(text) {
|
|
2
|
-
return `\
|
|
2
|
+
return `\u{1B}[1m${text}\u{1B}[0m`;
|
|
3
3
|
}
|
|
4
4
|
export function dim(text) {
|
|
5
|
-
return `\
|
|
5
|
+
return `\u{1B}[2m${text}\u{1B}[0m`;
|
|
6
6
|
}
|
|
7
7
|
export function sectionHeader(name) {
|
|
8
8
|
return `━━━ ${name} ━━━`;
|
|
@@ -22,7 +22,7 @@ export function renderInjectedReadme(readme, changelogJsonPath, tag, sectionOrde
|
|
|
22
22
|
const renderedSections = renderReleaseNotesSingle(entry, {
|
|
23
23
|
filter: matchesAudience('all'),
|
|
24
24
|
includeHeading: false,
|
|
25
|
-
...(sectionOrder
|
|
25
|
+
...(sectionOrder !== undefined && { sectionOrder }),
|
|
26
26
|
});
|
|
27
27
|
if (renderedSections.trimEnd().length === 0) {
|
|
28
28
|
console.warn(`Warning: no user-facing release notes for version ${version}; skipping README injection`);
|
|
@@ -85,9 +85,9 @@ export async function prepareCommand(argv) {
|
|
|
85
85
|
const options = {
|
|
86
86
|
dryRun,
|
|
87
87
|
force,
|
|
88
|
-
...(bumpOverride
|
|
89
|
-
...(setVersion
|
|
90
|
-
...(withReleaseNotes
|
|
88
|
+
...(bumpOverride !== undefined && { bumpOverride }),
|
|
89
|
+
...(setVersion !== undefined && { setVersion }),
|
|
90
|
+
...(withReleaseNotes && { withReleaseNotes: true }),
|
|
91
91
|
};
|
|
92
92
|
if (dryRun) {
|
|
93
93
|
console.info('\n🔍 DRY RUN — no files will be modified\n');
|
|
@@ -155,10 +155,11 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
|
|
|
155
155
|
if (only !== undefined) {
|
|
156
156
|
const knownNames = config.workspaces.map((w) => w.dir);
|
|
157
157
|
for (const name of only) {
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
process.exit(1);
|
|
158
|
+
if (knownNames.includes(name)) {
|
|
159
|
+
continue;
|
|
161
160
|
}
|
|
161
|
+
reportError(`Unknown workspace "${name}". Known workspaces: ${knownNames.join(', ')}`);
|
|
162
|
+
process.exit(1);
|
|
162
163
|
}
|
|
163
164
|
const graph = buildDependencyGraph(config.workspaces);
|
|
164
165
|
const violations = validateOnlyExcludesStrandedDependents(config.workspaces, only, graph, (workspace) => {
|
|
@@ -9,10 +9,7 @@ export async function previewTagPrefixes() {
|
|
|
9
9
|
const workspacePaths = (await discoverWorkspaces()) ?? [];
|
|
10
10
|
const userConfig = await loadUserConfig();
|
|
11
11
|
const overridesByDir = buildOverrideMap(userConfig);
|
|
12
|
-
const workspaces =
|
|
13
|
-
for (const workspacePath of workspacePaths) {
|
|
14
|
-
workspaces.push(buildPreviewRow(workspacePath, overridesByDir));
|
|
15
|
-
}
|
|
12
|
+
const workspaces = Array.from(workspacePaths, (workspacePath) => buildPreviewRow(workspacePath, overridesByDir));
|
|
16
13
|
const retiredPackages = buildRetiredPreviewEntries(userConfig?.retiredPackages ?? []);
|
|
17
14
|
const collisions = detectCollisions(workspaces);
|
|
18
15
|
const knownPrefixes = collectKnownPrefixes(workspaces, retiredPackages);
|
|
@@ -120,6 +117,6 @@ function buildRetiredPreviewEntries(retiredPackages) {
|
|
|
120
117
|
name: retired.name,
|
|
121
118
|
tagPrefix: retired.tagPrefix,
|
|
122
119
|
tagCount: countTagsMatching(retired.tagPrefix),
|
|
123
|
-
...(retired.successor !== undefined
|
|
120
|
+
...(retired.successor !== undefined && { successor: retired.successor }),
|
|
124
121
|
}));
|
|
125
122
|
}
|
|
@@ -95,8 +95,8 @@ export function releasePrepareMono(config, options) {
|
|
|
95
95
|
tags,
|
|
96
96
|
formatCommand,
|
|
97
97
|
dryRun,
|
|
98
|
-
...(allWarnings.length > 0
|
|
99
|
-
...(project
|
|
98
|
+
...(allWarnings.length > 0 && { warnings: allWarnings }),
|
|
99
|
+
...(project !== undefined && { project }),
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
function determineDirectBumps(config, options) {
|
|
@@ -487,10 +487,11 @@ function topologicalSort(releaseSet, graph) {
|
|
|
487
487
|
const sortedSet = new Set(sorted);
|
|
488
488
|
const cyclicDirs = [];
|
|
489
489
|
for (const dir of releaseDirs) {
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
cyclicDirs.push(dir);
|
|
490
|
+
if (sortedSet.has(dir)) {
|
|
491
|
+
continue;
|
|
493
492
|
}
|
|
493
|
+
sorted.push(dir);
|
|
494
|
+
cyclicDirs.push(dir);
|
|
494
495
|
}
|
|
495
496
|
return { sorted, cyclicDirs };
|
|
496
497
|
}
|
|
@@ -50,8 +50,8 @@ function sortSectionsByOrder(sections, order) {
|
|
|
50
50
|
}
|
|
51
51
|
const indexed = sections.map((section, index) => ({ section, index }));
|
|
52
52
|
indexed.sort((a, b) => {
|
|
53
|
-
const priorityA = priority.get(a.section.title) ??
|
|
54
|
-
const priorityB = priority.get(b.section.title) ??
|
|
53
|
+
const priorityA = priority.get(a.section.title) ?? Infinity;
|
|
54
|
+
const priorityB = priority.get(b.section.title) ?? Infinity;
|
|
55
55
|
if (priorityA !== priorityB) {
|
|
56
56
|
return priorityA - priorityB;
|
|
57
57
|
}
|
|
@@ -42,10 +42,11 @@ export async function resolveCommandTags(tags) {
|
|
|
42
42
|
if (tags !== undefined) {
|
|
43
43
|
const availableTagNames = resolvedTags.map((t) => t.tag);
|
|
44
44
|
for (const name of tags) {
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
process.exit(1);
|
|
45
|
+
if (availableTagNames.includes(name)) {
|
|
46
|
+
continue;
|
|
48
47
|
}
|
|
48
|
+
reportError(`Unknown tag "${name}" in --tags. Available: ${availableTagNames.join(', ')}`);
|
|
49
|
+
process.exit(1);
|
|
49
50
|
}
|
|
50
51
|
resolvedTags = resolvedTags.filter((t) => tags.includes(t.tag));
|
|
51
52
|
}
|
|
@@ -8,7 +8,7 @@ import { resolveLabels } from "./resolveLabels.js";
|
|
|
8
8
|
export const LABELS_OUTPUT_PATH = '.github/labels.yaml';
|
|
9
9
|
export function formatLabelsYaml(labels, presetHashes) {
|
|
10
10
|
const headerLines = ['# Generated by release-kit sync-labels — do not edit.', `# Source: ${SYNC_LABELS_CONFIG_PATH}`];
|
|
11
|
-
for (const [name, hash] of [...presetHashes
|
|
11
|
+
for (const [name, hash] of [...presetHashes].sort(([a], [b]) => a.localeCompare(b))) {
|
|
12
12
|
headerLines.push(`# ${name} preset hash: ${hash}`);
|
|
13
13
|
}
|
|
14
14
|
const yamlBody = stringify(labels, { singleQuote: true, lineWidth: 0 });
|
|
@@ -6,13 +6,37 @@ name: Sync labels
|
|
|
6
6
|
on:
|
|
7
7
|
workflow_dispatch:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# Apply on merge, so a regenerated labels file cannot sit unapplied.
|
|
10
|
+
push:
|
|
11
|
+
paths:
|
|
12
|
+
- .github/labels.yaml
|
|
12
13
|
|
|
14
|
+
# Preview on review: the check's log carries the full computed diff,
|
|
15
|
+
# including deletions of labels the file does not declare.
|
|
16
|
+
pull_request:
|
|
17
|
+
paths:
|
|
18
|
+
- .github/labels.yaml
|
|
19
|
+
|
|
20
|
+
# Permissions are fixed when a run is created and cannot vary by trigger within a job,
|
|
21
|
+
# so applying and previewing are separate jobs and the write token stays out of PR runs.
|
|
13
22
|
jobs:
|
|
23
|
+
# The push arm applies on the default branch alone. A \`branches:\` filter cannot name the
|
|
24
|
+
# default branch, so the gate lives on the job and this file stays identical across repos.
|
|
14
25
|
sync:
|
|
26
|
+
if: github.event_name != 'pull_request' && (github.event_name != 'push' || github.ref_name == github.event.repository.default_branch)
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
issues: write
|
|
30
|
+
uses: williamthorsen/node-monorepo-tools/.github/workflows/sync-labels.reusable.yaml@workflow/sync-labels-v1
|
|
31
|
+
|
|
32
|
+
check:
|
|
33
|
+
if: github.event_name == 'pull_request'
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
issues: read
|
|
15
37
|
uses: williamthorsen/node-monorepo-tools/.github/workflows/sync-labels.reusable.yaml@workflow/sync-labels-v1
|
|
38
|
+
with:
|
|
39
|
+
dry-run: true
|
|
16
40
|
`;
|
|
17
41
|
}
|
|
18
42
|
export function buildScopeLabels(workspacePaths) {
|
|
@@ -53,7 +53,7 @@ function writePreviewFile(filePath, content, dryRun) {
|
|
|
53
53
|
const result = writeFileWithCheck(filePath, content, { dryRun: false, overwrite: true });
|
|
54
54
|
if (result.outcome === 'failed') {
|
|
55
55
|
reportError(`Failed to write ${filePath}: ${result.error ?? 'unknown error'}`);
|
|
56
|
-
return { filePath, outcome: 'failed', ...(result.error
|
|
56
|
+
return { filePath, outcome: 'failed', ...(result.error !== undefined && { error: result.error }) };
|
|
57
57
|
}
|
|
58
58
|
if (result.outcome === 'created' || result.outcome === 'overwritten') {
|
|
59
59
|
console.info(dim(` Wrote ${filePath}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/release-kit",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"description": "Version-bumping and changelog-generation toolkit for release workflows",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/release-kit#readme",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"semver": "7.8.5",
|
|
40
40
|
"yaml": "2.9.0",
|
|
41
41
|
"zod": "4.4.3",
|
|
42
|
-
"@williamthorsen/nmr-core": "0.7.
|
|
42
|
+
"@williamthorsen/nmr-core": "0.7.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/semver": "7.7.1",
|
|
46
46
|
"smol-toml": "1.7.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
49
|
+
"node": ">=24"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public",
|