its-magic 0.1.2 → 0.1.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.
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env bash
2
+ # ─────────────────────────────────────────────────────────────────
3
+ # Unified release: npm + Chocolatey + Homebrew (all three at once)
4
+ # ─────────────────────────────────────────────────────────────────
5
+ set -euo pipefail
6
+
7
+ BUMP="patch"
8
+ NPM_TAG="latest"
9
+ SKIP_NPM=false
10
+ SKIP_CHOCO=false
11
+ SKIP_BREW=false
12
+ DRY_RUN=false
13
+
14
+ usage() {
15
+ cat <<EOF
16
+ Usage: $0 [options]
17
+ --bump <patch|minor|major|x.y.z> Version bump (default: patch)
18
+ --npm-tag <tag> npm dist-tag (default: latest)
19
+ --skip-npm Skip npm publish
20
+ --skip-choco Skip Chocolatey push
21
+ --skip-brew Skip Homebrew formula update
22
+ --dry-run Print actions without executing
23
+ -h|--help Show this help
24
+ EOF
25
+ exit 0
26
+ }
27
+
28
+ while [[ $# -gt 0 ]]; do
29
+ case "$1" in
30
+ --bump) BUMP="$2"; shift 2;;
31
+ --npm-tag) NPM_TAG="$2"; shift 2;;
32
+ --skip-npm) SKIP_NPM=true; shift;;
33
+ --skip-choco)SKIP_CHOCO=true; shift;;
34
+ --skip-brew) SKIP_BREW=true; shift;;
35
+ --dry-run) DRY_RUN=true; shift;;
36
+ -h|--help) usage;;
37
+ *) echo "Unknown option: $1"; usage;;
38
+ esac
39
+ done
40
+
41
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
42
+ cd "$REPO_ROOT"
43
+
44
+ # Node 22+: trust OS CA store when TLS inspection adds system CAs (Windows/antivirus/corp proxy).
45
+ export NODE_USE_SYSTEM_CA="${NODE_USE_SYSTEM_CA:-1}"
46
+
47
+ log() { echo -e "\033[36m[release]\033[0m $1"; }
48
+ warn() { echo -e "\033[33m[release]\033[0m $1"; }
49
+ err() { echo -e "\033[31m[release]\033[0m $1"; exit 1; }
50
+
51
+ # ── 1. Version bump ─────────────────────────────────────────────────
52
+ log "Bumping version ($BUMP) ..."
53
+ if $DRY_RUN; then
54
+ log "(dry-run) would run: npm version $BUMP --no-git-tag-version"
55
+ NEW_VERSION="0.0.0-dryrun"
56
+ else
57
+ npm version "$BUMP" --no-git-tag-version >/dev/null
58
+ NEW_VERSION=$(node -p "require('./package.json').version")
59
+ fi
60
+ TAG_NAME="v$NEW_VERSION"
61
+ log "New version: $NEW_VERSION (tag: $TAG_NAME)"
62
+ CHOCO_VERSION="$NEW_VERSION"
63
+ if [[ "$NEW_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-([0-9]+)$ ]]; then
64
+ # Old Chocolatey/NuGet rejects numeric-only prerelease labels (e.g. 0.1.1-1).
65
+ # Convert to a compatible label for nuspec only.
66
+ CHOCO_VERSION="${BASH_REMATCH[1]}-beta${BASH_REMATCH[2]}"
67
+ fi
68
+ if [[ "$CHOCO_VERSION" != "$NEW_VERSION" ]]; then
69
+ log "Chocolatey version normalized: $NEW_VERSION -> $CHOCO_VERSION"
70
+ fi
71
+
72
+ # ── 2. npm publish ──────────────────────────────────────────────────
73
+ if ! $SKIP_NPM; then
74
+ log "Publishing to npm (tag=$NPM_TAG) ..."
75
+ if $DRY_RUN; then
76
+ log "(dry-run) would run: npm publish --tag $NPM_TAG"
77
+ else
78
+ npm publish --tag "$NPM_TAG"
79
+ log "npm publish OK"
80
+ fi
81
+ else
82
+ warn "Skipping npm"
83
+ fi
84
+
85
+ # ── 3. GitHub release ───────────────────────────────────────────────
86
+ ZIP_URL=""
87
+ TAR_URL=""
88
+
89
+ GH_PRERELEASE=""
90
+ if [[ "$NEW_VERSION" == *-* ]]; then
91
+ GH_PRERELEASE="--prerelease"
92
+ fi
93
+
94
+ RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES="${RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES:-0}"
95
+ VERSION_NOTES="handoffs/releases/${NEW_VERSION}-release-notes.md"
96
+
97
+ if command -v gh &>/dev/null; then
98
+ log "Creating GitHub release $TAG_NAME ..."
99
+ if [[ ! -f "$VERSION_NOTES" ]]; then
100
+ log "Version notes missing — deriving via release_changelog_backfill --ensure-version ..."
101
+ if ! $DRY_RUN; then
102
+ python scripts/release_changelog_backfill.py --repo "$REPO_ROOT" --ensure-version "$NEW_VERSION" || true
103
+ fi
104
+ fi
105
+ if $DRY_RUN; then
106
+ if [[ -f "$VERSION_NOTES" ]]; then
107
+ log "(dry-run) would run: python scripts/release_changelog_validate.py --repo . --enforce"
108
+ log "(dry-run) would run: gh release create $TAG_NAME -F $VERSION_NOTES --title $TAG_NAME $GH_PRERELEASE"
109
+ else
110
+ log "(dry-run) would fail-closed: RELEASE_CHANGELOG_VERSION_DOC_MISSING ($VERSION_NOTES)"
111
+ fi
112
+ elif [[ -f "$VERSION_NOTES" ]]; then
113
+ python scripts/release_changelog_validate.py --repo "$REPO_ROOT" --enforce
114
+ gh release create "$TAG_NAME" -F "$VERSION_NOTES" --title "$TAG_NAME" $GH_PRERELEASE || warn "gh release create failed – continuing"
115
+ elif [[ "$RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES" == "1" ]]; then
116
+ warn "RELEASE_CHANGELOG_VERSION_DOC_MISSING: $VERSION_NOTES — falling back to --generate-notes (opt-in)"
117
+ gh release create "$TAG_NAME" --generate-notes --title "$TAG_NAME" $GH_PRERELEASE || warn "gh release create failed – continuing"
118
+ else
119
+ err "RELEASE_CHANGELOG_VERSION_DOC_MISSING: $VERSION_NOTES (set RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES=1 to opt into --generate-notes)"
120
+ fi
121
+ REMOTE_URL=$(git remote get-url origin 2>/dev/null || true)
122
+ if [[ "$REMOTE_URL" =~ github\.com[:/](.+?)(\.git)?$ ]]; then
123
+ REPO_SLUG="${BASH_REMATCH[1]}"
124
+ REPO_SLUG="${REPO_SLUG%.git}"
125
+ ZIP_URL="https://github.com/$REPO_SLUG/archive/refs/tags/$TAG_NAME.zip"
126
+ TAR_URL="https://github.com/$REPO_SLUG/archive/refs/tags/$TAG_NAME.tar.gz"
127
+ fi
128
+ else
129
+ warn "gh CLI not found – skipping GitHub release"
130
+ ZIP_URL="https://github.com/USER/its-magic/archive/refs/tags/$TAG_NAME.zip"
131
+ TAR_URL="https://github.com/USER/its-magic/archive/refs/tags/$TAG_NAME.tar.gz"
132
+ fi
133
+
134
+ # ── 4. Chocolatey ───────────────────────────────────────────────────
135
+ if ! $SKIP_CHOCO; then
136
+ NUSPEC="$REPO_ROOT/packaging/chocolatey/its-magic.nuspec"
137
+ CHOCO_INSTALL="$REPO_ROOT/packaging/chocolatey/tools/chocolateyInstall.ps1"
138
+
139
+ if [[ -f "$NUSPEC" ]]; then
140
+ log "Updating Chocolatey nuspec to $CHOCO_VERSION ..."
141
+ if ! $DRY_RUN; then
142
+ # Update version in nuspec (simple sed)
143
+ sed -i.bak "s|<version>[^<]*</version>|<version>$CHOCO_VERSION</version>|" "$NUSPEC"
144
+ rm -f "${NUSPEC}.bak"
145
+
146
+ # Update URL in install script
147
+ if [[ -n "$ZIP_URL" ]]; then
148
+ sed -i.bak "s|^\\$url[[:space:]]*=.*$|\\$url = '$ZIP_URL'|" "$CHOCO_INSTALL"
149
+ rm -f "${CHOCO_INSTALL}.bak"
150
+ fi
151
+
152
+ # Compute checksum
153
+ if [[ -n "$ZIP_URL" ]]; then
154
+ TMP_ZIP="/tmp/its-magic-$TAG_NAME.zip"
155
+ if curl -fsSL "$ZIP_URL" -o "$TMP_ZIP" 2>/dev/null; then
156
+ SHA=$(shasum -a 256 "$TMP_ZIP" | awk '{print $1}')
157
+ sed -i.bak "s|^\\$checksum[[:space:]]*=.*$|\\$checksum = '$SHA'|" "$CHOCO_INSTALL"
158
+ rm -f "${CHOCO_INSTALL}.bak" "$TMP_ZIP"
159
+ log "Chocolatey checksum: $SHA"
160
+ else
161
+ warn "Could not download zip for checksum – set PLACEHOLDER manually"
162
+ fi
163
+ fi
164
+ fi
165
+
166
+ if command -v choco &>/dev/null; then
167
+ log "Packing + pushing Chocolatey package ..."
168
+ if ! $DRY_RUN; then
169
+ (cd "$REPO_ROOT/packaging/chocolatey" && choco pack && \
170
+ NUPKG=$(ls -1t *.nupkg 2>/dev/null | head -1) && \
171
+ choco push "$NUPKG" --source https://push.chocolatey.org/ || warn "choco push failed")
172
+ fi
173
+ else
174
+ warn "choco not found – nuspec updated but not pushed"
175
+ fi
176
+ else
177
+ warn "nuspec not found – skipping Chocolatey"
178
+ fi
179
+ else
180
+ warn "Skipping Chocolatey"
181
+ fi
182
+
183
+ # ── 5. Homebrew ─────────────────────────────────────────────────────
184
+ if ! $SKIP_BREW; then
185
+ # Detect pre-release: anything with a hyphen (e.g. 0.2.0-beta.1, 1.0.0-rc.1)
186
+ IS_PRERELEASE=false
187
+ if [[ "$NEW_VERSION" == *-* ]]; then
188
+ IS_PRERELEASE=true
189
+ fi
190
+
191
+ if $IS_PRERELEASE; then
192
+ FORMULA="$REPO_ROOT/packaging/homebrew/its-magic-beta.rb"
193
+ log "Pre-release detected – using beta formula"
194
+ else
195
+ FORMULA="$REPO_ROOT/packaging/homebrew/its-magic.rb"
196
+ fi
197
+
198
+ if [[ -f "$FORMULA" ]]; then
199
+ FORMULA_NAME=$(basename "$FORMULA")
200
+ log "Updating Homebrew formula ($FORMULA_NAME) to $NEW_VERSION ..."
201
+ if ! $DRY_RUN; then
202
+ if [[ -n "$TAR_URL" ]]; then
203
+ sed -i.bak "s|url \"https://github.com/[^\"]*\.tar\.gz\"|url \"$TAR_URL\"|" "$FORMULA"
204
+ rm -f "${FORMULA}.bak"
205
+ else
206
+ sed -i.bak "s|vVERSION|$TAG_NAME|g" "$FORMULA"
207
+ rm -f "${FORMULA}.bak"
208
+ fi
209
+
210
+ # Update explicit version line for beta formula
211
+ if $IS_PRERELEASE; then
212
+ sed -i.bak "s|version \"[^\"]*\"|version \"$NEW_VERSION\"|" "$FORMULA"
213
+ rm -f "${FORMULA}.bak"
214
+ fi
215
+
216
+ # Compute sha256
217
+ if [[ -n "$TAR_URL" ]]; then
218
+ TMP_TAR="/tmp/its-magic-$TAG_NAME.tar.gz"
219
+ if curl -fsSL "$TAR_URL" -o "$TMP_TAR" 2>/dev/null; then
220
+ SHA=$(shasum -a 256 "$TMP_TAR" | awk '{print $1}')
221
+ # Replace both PLACEHOLDER and any previous sha256 value
222
+ sed -i.bak "s|sha256 \"[^\"]*\"|sha256 \"$SHA\"|" "$FORMULA"
223
+ rm -f "${FORMULA}.bak" "$TMP_TAR"
224
+ log "Homebrew sha256: $SHA"
225
+ else
226
+ warn "Could not download tar.gz – set sha256 manually"
227
+ fi
228
+ fi
229
+ log "Homebrew formula updated: $FORMULA_NAME"
230
+ if $IS_PRERELEASE; then
231
+ log "Users install beta with: brew install USER/tap/its-magic-beta"
232
+ else
233
+ log "Users install stable with: brew install USER/tap/its-magic"
234
+ fi
235
+ fi
236
+ else
237
+ warn "Formula not found – skipping Homebrew"
238
+ fi
239
+ else
240
+ warn "Skipping Homebrew"
241
+ fi
242
+
243
+ # ── Done ─────────────────────────────────────────────────────────────
244
+ log "=========================================="
245
+ log "Release $NEW_VERSION complete!"
246
+ log " npm: $(if $SKIP_NPM; then echo 'SKIPPED'; else echo 'OK'; fi)"
247
+ log " Chocolatey: $(if $SKIP_CHOCO; then echo 'SKIPPED'; else echo 'OK (check warnings)'; fi)"
248
+ log " Homebrew: $(if $SKIP_BREW; then echo 'SKIPPED'; else echo 'OK (formula updated)'; fi)"
249
+ log "=========================================="
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Idempotent one-time backfill for release changelog tiers A/B/C (US-0100 / DEC-0085).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import os
10
+ import re
11
+ import sys
12
+
13
+ _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
14
+ _REPO_ROOT = os.path.normpath(os.path.join(_SCRIPT_DIR, ".."))
15
+ if _REPO_ROOT not in sys.path:
16
+ sys.path.insert(0, _REPO_ROOT)
17
+ if _SCRIPT_DIR not in sys.path:
18
+ sys.path.insert(0, _SCRIPT_DIR)
19
+
20
+ import release_changelog_lib as rcl # noqa: E402
21
+
22
+ try:
23
+ import yaml # type: ignore
24
+ except ImportError:
25
+ yaml = None # type: ignore
26
+
27
+
28
+ MANIFEST_REL = os.path.join(
29
+ "docs", "engineering", "context", "release-version-backfill.manifest.yaml"
30
+ )
31
+
32
+
33
+ def _load_manifest(repo_root: str) -> dict:
34
+ path = os.path.join(repo_root, MANIFEST_REL)
35
+ if not os.path.isfile(path):
36
+ return {"schema_version": 1, "entries": []}
37
+ text = rcl.read_utf8(path)
38
+ if yaml is not None:
39
+ data = yaml.safe_load(text) or {}
40
+ else:
41
+ # Minimal stdlib fallback for schema_version + entries
42
+ data = {"schema_version": 1, "entries": []}
43
+ m = re.search(r"schema_version:\s*(\d+)", text)
44
+ if m:
45
+ data["schema_version"] = int(m.group(1))
46
+ for em in re.finditer(
47
+ r"-\s*sprint_id:\s*(S\d{4})\s*\n\s*semver:\s*[\"']?([^\"'\n]+)[\"']?",
48
+ text,
49
+ ):
50
+ data.setdefault("entries", []).append(
51
+ {"sprint_id": em.group(1), "semver": em.group(2).strip()}
52
+ )
53
+ return data
54
+
55
+
56
+ def _synthetic_semver(sprint_id: str) -> str:
57
+ num = int(sprint_id[1:])
58
+ return f"0.0.0-wf.{num:03d}"
59
+
60
+
61
+ def assign_semvers(repo_root: str) -> dict[str, str]:
62
+ """Tier A → B → C precedence; returns sprint_id → semver."""
63
+ rows = sorted(
64
+ [r for r in rcl.parse_queue_rows(repo_root) if r.status == "released"],
65
+ key=lambda r: r.last_updated,
66
+ )
67
+ manifest = _load_manifest(repo_root)
68
+ tier_b: dict[str, str] = {}
69
+ for entry in manifest.get("entries") or []:
70
+ sid = entry.get("sprint_id", "").strip()
71
+ sem = entry.get("semver", "").strip()
72
+ if sid and sem:
73
+ tier_b[sid] = rcl.normalize_semver(sem)
74
+ # collision detection
75
+ inv: dict[str, list[str]] = {}
76
+ for sid, sem in tier_b.items():
77
+ inv.setdefault(sem, []).append(sid)
78
+ for sem, sids in inv.items():
79
+ if len(sids) > 1:
80
+ raise rcl.ReleaseChangelogError(
81
+ rcl.RELEASE_CHANGELOG_BACKFILL_AMBIGUOUS,
82
+ f"manifest maps {sids} → {sem}",
83
+ )
84
+
85
+ assignment: dict[str, str] = {}
86
+ for row in rows:
87
+ if row.release_version.strip():
88
+ assignment[row.sprint_id] = rcl.normalize_semver(row.release_version)
89
+ elif row.sprint_id in tier_b:
90
+ assignment[row.sprint_id] = tier_b[row.sprint_id]
91
+ for row in rows:
92
+ if row.sprint_id in assignment:
93
+ continue
94
+ assignment[row.sprint_id] = _synthetic_semver(row.sprint_id)
95
+ return assignment
96
+
97
+
98
+ def run_backfill(repo_root: str, dry_run: bool = False) -> int:
99
+ rcl.ensure_changelog_stub(repo_root)
100
+ try:
101
+ assignment = assign_semvers(repo_root)
102
+ except rcl.ReleaseChangelogError as exc:
103
+ print(f"{exc.code}: {exc}", file=sys.stderr)
104
+ return 1
105
+
106
+ # coalesce by semver
107
+ groups: dict[str, list[str]] = {}
108
+ for sid, sem in assignment.items():
109
+ groups.setdefault(sem, []).append(sid)
110
+
111
+ for semver, sprint_ids in sorted(groups.items()):
112
+ if dry_run:
113
+ print(f"would build {semver} <- {sprint_ids}")
114
+ continue
115
+ try:
116
+ rcl.build_version_doc(semver, sprint_ids, repo_root)
117
+ rcl.promote_unreleased(semver, sprint_ids, repo_root)
118
+ rcl.bind_queue_release_version(sprint_ids, semver, repo_root)
119
+ except rcl.ReleaseChangelogError as exc:
120
+ if exc.code == rcl.RELEASE_CHANGELOG_DUPLICATE_VERSION:
121
+ print(rcl.RELEASE_CHANGELOG_IDEMPOTENCY_OK, file=sys.stderr)
122
+ continue
123
+ print(f"{exc.code}: {exc}", file=sys.stderr)
124
+ return 1
125
+
126
+ print("[RELEASE_CHANGELOG_BACKFILL_OK]")
127
+ return 0
128
+
129
+
130
+ def main() -> int:
131
+ parser = argparse.ArgumentParser(description=__doc__)
132
+ parser.add_argument("--repo", default=_REPO_ROOT)
133
+ parser.add_argument("--dry-run", action="store_true")
134
+ parser.add_argument(
135
+ "--ensure-version",
136
+ metavar="SEMVER",
137
+ help="Ensure handoffs/releases/{semver}-release-notes.md exists (release-all.sh).",
138
+ )
139
+ args = parser.parse_args()
140
+ root = os.path.abspath(args.repo)
141
+ if args.ensure_version:
142
+ try:
143
+ path = rcl.ensure_version_doc_for_release(args.ensure_version, root)
144
+ print(path)
145
+ return 0
146
+ except rcl.ReleaseChangelogError as exc:
147
+ print(f"{exc.code}: {exc}", file=sys.stderr)
148
+ return 1
149
+ return run_backfill(root, dry_run=args.dry_run)
150
+
151
+
152
+ if __name__ == "__main__":
153
+ raise SystemExit(main())