rlsbl 0.2.0 → 0.3.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/package.json +1 -1
- package/rlsbl/__init__.py +22 -6
- package/rlsbl/commands/config.py +48 -0
- package/rlsbl/commands/init_cmd.py +142 -56
- package/rlsbl/commands/release.py +24 -5
- package/rlsbl/commands/undo.py +66 -0
- package/templates/merged/publish.yml.tpl +30 -0
package/package.json
CHANGED
package/rlsbl/__init__.py
CHANGED
|
@@ -10,17 +10,19 @@ except Exception:
|
|
|
10
10
|
__version__ = "unknown"
|
|
11
11
|
|
|
12
12
|
REGISTRIES = ("npm", "pypi")
|
|
13
|
-
COMMANDS = ("release", "status", "scaffold", "check")
|
|
13
|
+
COMMANDS = ("release", "status", "scaffold", "check", "config", "undo")
|
|
14
14
|
COMMAND_ALIASES = {"init": "scaffold"}
|
|
15
15
|
|
|
16
16
|
HELP = f"""\
|
|
17
17
|
rlsbl v{__version__} -- Release orchestration and project scaffolding for npm and PyPI
|
|
18
18
|
|
|
19
19
|
Usage:
|
|
20
|
-
rlsbl release [patch|minor|major] [--dry-run] [--quiet] Orchestrate a release
|
|
20
|
+
rlsbl release [patch|minor|major] [--dry-run] [--yes] [--quiet] Orchestrate a release
|
|
21
21
|
rlsbl status Show project status
|
|
22
22
|
rlsbl scaffold [--force] [--update] Scaffold release infrastructure
|
|
23
23
|
rlsbl check <name> Check name availability
|
|
24
|
+
rlsbl config Show project configuration
|
|
25
|
+
rlsbl undo [--yes] Revert the last release
|
|
24
26
|
|
|
25
27
|
Options:
|
|
26
28
|
--registry <npm|pypi> Target a specific registry (auto-detected if omitted)
|
|
@@ -80,6 +82,8 @@ def _get_command_module(command):
|
|
|
80
82
|
"status": "status",
|
|
81
83
|
"scaffold": "init_cmd",
|
|
82
84
|
"check": "check",
|
|
85
|
+
"config": "config",
|
|
86
|
+
"undo": "undo",
|
|
83
87
|
}
|
|
84
88
|
module_name = module_map.get(command)
|
|
85
89
|
if not module_name:
|
|
@@ -153,10 +157,22 @@ def main():
|
|
|
153
157
|
print("Error: no package.json or pyproject.toml found.", file=sys.stderr)
|
|
154
158
|
sys.exit(1)
|
|
155
159
|
if len(regs) > 1:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
handler.run_cmd_multi(regs, args, flags)
|
|
161
|
+
else:
|
|
162
|
+
handler.run_cmd(regs[0], args, flags)
|
|
163
|
+
elif command == "config":
|
|
164
|
+
# config: auto-detect, pass first registry or fallback
|
|
165
|
+
regs = detect_registries()
|
|
166
|
+
handler.run_cmd(registry or (regs[0] if regs else "npm"), args, flags)
|
|
167
|
+
elif command == "undo":
|
|
168
|
+
# undo: auto-detect like release
|
|
169
|
+
if not registry:
|
|
170
|
+
regs = detect_registries()
|
|
171
|
+
if not regs:
|
|
172
|
+
print("Error: no package.json or pyproject.toml found.", file=sys.stderr)
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
registry = regs[0]
|
|
175
|
+
handler.run_cmd(registry, args, flags)
|
|
160
176
|
else:
|
|
161
177
|
# release, status: use explicit registry or auto-detect primary
|
|
162
178
|
if not registry:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Config command: show resolved project configuration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from ..registries import REGISTRIES
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run_cmd(registry, args, flags):
|
|
8
|
+
print("Detected registries:")
|
|
9
|
+
for name, reg in REGISTRIES.items():
|
|
10
|
+
if reg.check_project_exists("."):
|
|
11
|
+
version = reg.read_version(".")
|
|
12
|
+
print(f" {name}: {reg.get_version_file()} (v{version})")
|
|
13
|
+
else:
|
|
14
|
+
print(f" {name}: not found")
|
|
15
|
+
|
|
16
|
+
print("\nScaffolding:")
|
|
17
|
+
rlsbl_dir = os.path.join(".", ".rlsbl")
|
|
18
|
+
if os.path.isdir(rlsbl_dir):
|
|
19
|
+
version_file = os.path.join(rlsbl_dir, "version")
|
|
20
|
+
if os.path.exists(version_file):
|
|
21
|
+
with open(version_file) as f:
|
|
22
|
+
scaffold_ver = f.read().strip()
|
|
23
|
+
print(f" Version marker: {scaffold_ver}")
|
|
24
|
+
hashes_file = os.path.join(rlsbl_dir, "hashes.json")
|
|
25
|
+
if os.path.exists(hashes_file):
|
|
26
|
+
import json
|
|
27
|
+
with open(hashes_file) as f:
|
|
28
|
+
hashes = json.load(f)
|
|
29
|
+
print(f" Tracked files: {len(hashes)}")
|
|
30
|
+
for path in sorted(hashes):
|
|
31
|
+
print(f" {path}")
|
|
32
|
+
else:
|
|
33
|
+
print(" Not scaffolded (run 'rlsbl scaffold')")
|
|
34
|
+
|
|
35
|
+
print("\nWorkflows:")
|
|
36
|
+
for wf in ["ci.yml", "publish.yml", "workflow.yml"]:
|
|
37
|
+
path = os.path.join(".github", "workflows", wf)
|
|
38
|
+
print(f" {wf}: {'yes' if os.path.exists(path) else 'no'}")
|
|
39
|
+
|
|
40
|
+
print("\nHooks:")
|
|
41
|
+
pre_release = os.path.join("scripts", "pre-release.sh")
|
|
42
|
+
print(f" pre-release.sh: {'yes' if os.path.exists(pre_release) else 'no'}")
|
|
43
|
+
pre_push = os.path.join(".git", "hooks", "pre-push")
|
|
44
|
+
print(f" pre-push hook: {'installed' if os.path.exists(pre_push) else 'not installed'}")
|
|
45
|
+
|
|
46
|
+
print("\nFiles:")
|
|
47
|
+
for f in ["CHANGELOG.md", "LICENSE", ".gitignore", "CLAUDE.md"]:
|
|
48
|
+
print(f" {f}: {'yes' if os.path.exists(f) else 'no'}")
|
|
@@ -208,52 +208,12 @@ def process_mappings(template_dir, mappings, vars_dict, force, update=False,
|
|
|
208
208
|
return created, skipped, warnings, new_hashes
|
|
209
209
|
|
|
210
210
|
|
|
211
|
-
def
|
|
212
|
-
|
|
211
|
+
def _finalize_scaffold(existing_hashes, all_hash_dicts, created, skipped, warnings,
|
|
212
|
+
registry=None):
|
|
213
|
+
"""Shared post-processing for scaffold: chmod, hooks, version marker, hashes, summary.
|
|
213
214
|
|
|
214
|
-
|
|
215
|
-
from templates.
|
|
215
|
+
all_hash_dicts is a list of dicts to merge into existing_hashes.
|
|
216
216
|
"""
|
|
217
|
-
reg = REGISTRIES[registry]
|
|
218
|
-
|
|
219
|
-
# Check that a project file exists
|
|
220
|
-
if not reg.check_project_exists("."):
|
|
221
|
-
print(f"Error: no {registry} project found in current directory.", file=sys.stderr)
|
|
222
|
-
print(reg.get_project_init_hint(), file=sys.stderr)
|
|
223
|
-
sys.exit(1)
|
|
224
|
-
|
|
225
|
-
# Gather template variables
|
|
226
|
-
vars_dict = reg.get_template_vars(".")
|
|
227
|
-
from datetime import datetime
|
|
228
|
-
vars_dict["year"] = str(datetime.now().year)
|
|
229
|
-
|
|
230
|
-
force = flags.get("force", False)
|
|
231
|
-
update = flags.get("update", False)
|
|
232
|
-
|
|
233
|
-
existing_hashes = load_hashes()
|
|
234
|
-
|
|
235
|
-
# Process registry-specific templates
|
|
236
|
-
reg_created, reg_skipped, reg_warnings, reg_hashes = process_mappings(
|
|
237
|
-
reg.get_template_dir(),
|
|
238
|
-
reg.get_template_mappings(),
|
|
239
|
-
vars_dict,
|
|
240
|
-
force,
|
|
241
|
-
update,
|
|
242
|
-
existing_hashes,
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
# Process shared templates (skip if another registry already handled them)
|
|
246
|
-
shared_created, shared_skipped, shared_warnings, shared_hashes = [], [], [], {}
|
|
247
|
-
if not flags.get("skip-shared"):
|
|
248
|
-
shared_created, shared_skipped, shared_warnings, shared_hashes = process_mappings(
|
|
249
|
-
reg.get_shared_template_dir(),
|
|
250
|
-
reg.get_shared_template_mappings(),
|
|
251
|
-
vars_dict,
|
|
252
|
-
force,
|
|
253
|
-
update,
|
|
254
|
-
existing_hashes,
|
|
255
|
-
)
|
|
256
|
-
|
|
257
217
|
# Make all shell scripts in scripts/ executable
|
|
258
218
|
scripts_dir = os.path.join(".", "scripts")
|
|
259
219
|
if os.path.isdir(scripts_dir):
|
|
@@ -282,16 +242,11 @@ def run_cmd(registry, args, flags):
|
|
|
282
242
|
|
|
283
243
|
# Persist file hashes for future --update customization detection
|
|
284
244
|
all_new_hashes = {}
|
|
285
|
-
|
|
286
|
-
|
|
245
|
+
for h in all_hash_dicts:
|
|
246
|
+
all_new_hashes.update(h)
|
|
287
247
|
existing_hashes.update(all_new_hashes)
|
|
288
248
|
save_hashes(existing_hashes)
|
|
289
249
|
|
|
290
|
-
# Merge results
|
|
291
|
-
created = reg_created + shared_created
|
|
292
|
-
skipped = reg_skipped + shared_skipped
|
|
293
|
-
warnings = reg_warnings + shared_warnings
|
|
294
|
-
|
|
295
250
|
# Print summary
|
|
296
251
|
if created:
|
|
297
252
|
print("Created:")
|
|
@@ -314,8 +269,139 @@ def run_cmd(registry, args, flags):
|
|
|
314
269
|
print("\nNote: Existing CI workflow preserved. Review and merge manually if needed.")
|
|
315
270
|
|
|
316
271
|
# Next steps
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
272
|
+
if registry:
|
|
273
|
+
steps = NEXT_STEPS.get(registry)
|
|
274
|
+
if steps:
|
|
275
|
+
print("\nNext steps:")
|
|
276
|
+
for i, step in enumerate(steps, 1):
|
|
277
|
+
print(f" {i}. {step}")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def run_cmd(registry, args, flags):
|
|
281
|
+
"""Init command handler.
|
|
282
|
+
|
|
283
|
+
Scaffolds release infrastructure (CI, publish workflows, changelog, etc.)
|
|
284
|
+
from templates.
|
|
285
|
+
"""
|
|
286
|
+
reg = REGISTRIES[registry]
|
|
287
|
+
|
|
288
|
+
# Check that a project file exists
|
|
289
|
+
if not reg.check_project_exists("."):
|
|
290
|
+
print(f"Error: no {registry} project found in current directory.", file=sys.stderr)
|
|
291
|
+
print(reg.get_project_init_hint(), file=sys.stderr)
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
|
|
294
|
+
# Gather template variables
|
|
295
|
+
vars_dict = reg.get_template_vars(".")
|
|
296
|
+
from datetime import datetime
|
|
297
|
+
vars_dict["year"] = str(datetime.now().year)
|
|
298
|
+
|
|
299
|
+
force = flags.get("force", False)
|
|
300
|
+
update = flags.get("update", False)
|
|
301
|
+
|
|
302
|
+
existing_hashes = load_hashes()
|
|
303
|
+
|
|
304
|
+
# Process registry-specific templates
|
|
305
|
+
reg_created, reg_skipped, reg_warnings, reg_hashes = process_mappings(
|
|
306
|
+
reg.get_template_dir(),
|
|
307
|
+
reg.get_template_mappings(),
|
|
308
|
+
vars_dict,
|
|
309
|
+
force,
|
|
310
|
+
update,
|
|
311
|
+
existing_hashes,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Process shared templates (skip if another registry already handled them)
|
|
315
|
+
shared_created, shared_skipped, shared_warnings, shared_hashes = [], [], [], {}
|
|
316
|
+
if not flags.get("skip-shared"):
|
|
317
|
+
shared_created, shared_skipped, shared_warnings, shared_hashes = process_mappings(
|
|
318
|
+
reg.get_shared_template_dir(),
|
|
319
|
+
reg.get_shared_template_mappings(),
|
|
320
|
+
vars_dict,
|
|
321
|
+
force,
|
|
322
|
+
update,
|
|
323
|
+
existing_hashes,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
created = reg_created + shared_created
|
|
327
|
+
skipped = reg_skipped + shared_skipped
|
|
328
|
+
warnings = reg_warnings + shared_warnings
|
|
329
|
+
|
|
330
|
+
_finalize_scaffold(
|
|
331
|
+
existing_hashes, [reg_hashes, shared_hashes],
|
|
332
|
+
created, skipped, warnings, registry=registry,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def run_cmd_multi(registries_list, args, flags):
|
|
337
|
+
"""Scaffold for multiple registries with a merged publish workflow.
|
|
338
|
+
|
|
339
|
+
Uses the primary registry for template vars and CI, then writes a merged
|
|
340
|
+
publish.yml that contains jobs for all detected registries.
|
|
341
|
+
"""
|
|
342
|
+
primary = registries_list[0]
|
|
343
|
+
reg = REGISTRIES[primary]
|
|
344
|
+
|
|
345
|
+
if not reg.check_project_exists("."):
|
|
346
|
+
print(f"Error: no {primary} project found in current directory.", file=sys.stderr)
|
|
347
|
+
sys.exit(1)
|
|
348
|
+
|
|
349
|
+
print(f"Multiple registries detected: {', '.join(registries_list)}")
|
|
350
|
+
print("Scaffolding with merged publish workflow.")
|
|
351
|
+
|
|
352
|
+
vars_dict = reg.get_template_vars(".")
|
|
353
|
+
from datetime import datetime
|
|
354
|
+
vars_dict["year"] = str(datetime.now().year)
|
|
355
|
+
|
|
356
|
+
force = flags.get("force", False)
|
|
357
|
+
update = flags.get("update", False)
|
|
358
|
+
existing_hashes = load_hashes()
|
|
359
|
+
|
|
360
|
+
# Process primary registry CI template only (publish will come from merged)
|
|
361
|
+
ci_mappings = [m for m in reg.get_template_mappings() if "publish" not in m["template"]]
|
|
362
|
+
ci_created, ci_skipped, ci_warnings, ci_hashes = process_mappings(
|
|
363
|
+
reg.get_template_dir(),
|
|
364
|
+
ci_mappings,
|
|
365
|
+
vars_dict,
|
|
366
|
+
force,
|
|
367
|
+
update,
|
|
368
|
+
existing_hashes,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Process merged publish workflow template
|
|
372
|
+
merged_tpl_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
|
373
|
+
"..", "templates", "merged")
|
|
374
|
+
merged_created, merged_skipped, merged_warnings, merged_hashes = process_mappings(
|
|
375
|
+
merged_tpl_dir,
|
|
376
|
+
[{"template": "publish.yml.tpl", "target": ".github/workflows/publish.yml"}],
|
|
377
|
+
vars_dict,
|
|
378
|
+
force,
|
|
379
|
+
update,
|
|
380
|
+
existing_hashes,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Process shared templates (once)
|
|
384
|
+
shared_created, shared_skipped, shared_warnings, shared_hashes = process_mappings(
|
|
385
|
+
reg.get_shared_template_dir(),
|
|
386
|
+
reg.get_shared_template_mappings(),
|
|
387
|
+
vars_dict,
|
|
388
|
+
force,
|
|
389
|
+
update,
|
|
390
|
+
existing_hashes,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
created = ci_created + merged_created + shared_created
|
|
394
|
+
skipped = ci_skipped + merged_skipped + shared_skipped
|
|
395
|
+
warnings = ci_warnings + merged_warnings + shared_warnings
|
|
396
|
+
|
|
397
|
+
_finalize_scaffold(
|
|
398
|
+
existing_hashes, [ci_hashes, merged_hashes, shared_hashes],
|
|
399
|
+
created, skipped, warnings,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Show combined next steps for dual-registry
|
|
403
|
+
print("\nNext steps:")
|
|
404
|
+
print(" 1. Add an NPM_TOKEN secret to your GitHub repo (Settings > Secrets > Actions)")
|
|
405
|
+
print(" 2. Configure Trusted Publishing on pypi.org")
|
|
406
|
+
print(" 3. Push to GitHub to activate the CI workflow")
|
|
407
|
+
print(" 4. Run rlsbl release [patch|minor|major]")
|
|
@@ -127,21 +127,40 @@ def run_cmd(registry, args, flags):
|
|
|
127
127
|
log("--- No changes made ---")
|
|
128
128
|
return
|
|
129
129
|
|
|
130
|
-
#
|
|
130
|
+
# Pre-compute which files will be modified
|
|
131
131
|
version_file = reg.get_version_file()
|
|
132
|
+
files_to_commit = [version_file]
|
|
133
|
+
for name, other_reg in REGISTRIES.items():
|
|
134
|
+
if name == registry:
|
|
135
|
+
continue
|
|
136
|
+
if other_reg.check_project_exists("."):
|
|
137
|
+
files_to_commit.append(other_reg.get_version_file())
|
|
138
|
+
|
|
139
|
+
# Confirmation prompt (skip with --yes)
|
|
140
|
+
if not flags.get("yes"):
|
|
141
|
+
print(f"\nAbout to release {new_version} ({bump_type}) on {branch}")
|
|
142
|
+
print(f" Tag: {tag}")
|
|
143
|
+
print(f" Files: {', '.join(files_to_commit)}")
|
|
144
|
+
try:
|
|
145
|
+
answer = input("Proceed? [y/N] ").strip().lower()
|
|
146
|
+
except (EOFError, KeyboardInterrupt):
|
|
147
|
+
print("\nAborted.")
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
if answer != "y":
|
|
150
|
+
print("Aborted.")
|
|
151
|
+
sys.exit(0)
|
|
152
|
+
|
|
153
|
+
# Write new version to the primary registry file
|
|
132
154
|
reg.write_version(".", new_version)
|
|
133
155
|
log(f"Updated version in {version_file}")
|
|
134
156
|
|
|
135
157
|
# Sync version to all other recognized version files
|
|
136
|
-
files_to_commit = [version_file]
|
|
137
158
|
for name, other_reg in REGISTRIES.items():
|
|
138
159
|
if name == registry:
|
|
139
160
|
continue
|
|
140
161
|
if other_reg.check_project_exists("."):
|
|
141
162
|
other_reg.write_version(".", new_version)
|
|
142
|
-
|
|
143
|
-
files_to_commit.append(other_file)
|
|
144
|
-
log(f"Synced version to {other_file}")
|
|
163
|
+
log(f"Synced version to {other_reg.get_version_file()}")
|
|
145
164
|
|
|
146
165
|
# Commit all bumped version files together
|
|
147
166
|
commit_tool = find_commit_tool()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Undo command: revert the last release."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from ..utils import run, run_silent, check_gh_installed, check_gh_auth
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_cmd(registry, args, flags):
|
|
9
|
+
check_gh_installed()
|
|
10
|
+
check_gh_auth()
|
|
11
|
+
|
|
12
|
+
# Find the latest tag
|
|
13
|
+
try:
|
|
14
|
+
tag = run("git", ["describe", "--tags", "--abbrev=0"])
|
|
15
|
+
except Exception:
|
|
16
|
+
print("Error: no tags found. Nothing to undo.", file=sys.stderr)
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
print(f"This will undo release {tag}:")
|
|
20
|
+
print(f" - Delete git tag {tag} (local + remote)")
|
|
21
|
+
print(f" - Revert the version bump commit")
|
|
22
|
+
print(f" - Delete the GitHub Release for {tag}")
|
|
23
|
+
|
|
24
|
+
if not flags.get("yes"):
|
|
25
|
+
try:
|
|
26
|
+
answer = input("\nThis is destructive. Proceed? [y/N] ").strip().lower()
|
|
27
|
+
except (EOFError, KeyboardInterrupt):
|
|
28
|
+
print("\nAborted.")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
if answer != "y":
|
|
31
|
+
print("Aborted.")
|
|
32
|
+
sys.exit(0)
|
|
33
|
+
|
|
34
|
+
# Delete GitHub Release
|
|
35
|
+
try:
|
|
36
|
+
run("gh", ["release", "delete", tag, "--yes"])
|
|
37
|
+
print(f"Deleted GitHub Release: {tag}")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Warning: could not delete GitHub Release: {e}")
|
|
40
|
+
|
|
41
|
+
# Delete remote tag
|
|
42
|
+
try:
|
|
43
|
+
run("git", ["push", "origin", f":{tag}"])
|
|
44
|
+
print(f"Deleted remote tag: {tag}")
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"Warning: could not delete remote tag: {e}")
|
|
47
|
+
|
|
48
|
+
# Delete local tag
|
|
49
|
+
try:
|
|
50
|
+
run("git", ["tag", "-d", tag])
|
|
51
|
+
print(f"Deleted local tag: {tag}")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(f"Warning: could not delete local tag: {e}")
|
|
54
|
+
|
|
55
|
+
# Revert the version bump commit (should be HEAD)
|
|
56
|
+
try:
|
|
57
|
+
head_msg = run("git", ["log", "-1", "--format=%s"])
|
|
58
|
+
if head_msg == tag:
|
|
59
|
+
run("git", ["revert", "--no-edit", "HEAD"])
|
|
60
|
+
print(f"Reverted commit: {head_msg}")
|
|
61
|
+
else:
|
|
62
|
+
print(f"Warning: HEAD commit ({head_msg}) doesn't match tag ({tag}). Skipping revert.")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"Warning: could not revert commit: {e}")
|
|
65
|
+
|
|
66
|
+
print(f"\nUndo complete. Run 'git push' to sync the revert.")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
npm:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
|
+
- uses: actions/setup-node@v5
|
|
17
|
+
with:
|
|
18
|
+
node-version: 22
|
|
19
|
+
registry-url: https://registry.npmjs.org
|
|
20
|
+
- run: npm publish --provenance --access public
|
|
21
|
+
env:
|
|
22
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
23
|
+
|
|
24
|
+
pypi:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v5
|
|
28
|
+
- uses: astral-sh/setup-uv@v7
|
|
29
|
+
- run: uv build
|
|
30
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|