@krx3d/tizentube2 1.30.10 → 1.30.40

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.
@@ -9,8 +9,8 @@
9
9
  # - dist/userScript.js (browser userscript, bundled & minified)
10
10
  # - dist/service.js (DIAL service, bundled with Rollup)
11
11
  # - Publishes the package to https://www.npmjs.com on release only
12
- # - Automatically removes old versions from npm (within 72-hour window)
13
- # - Deprecates older versions (beyond 72-hour window) if any remain
12
+ # - Deprecates older versions once the new one is confirmed live on npm
13
+ # - Fails the run if npm's `latest` tag does not match what it published
14
14
  # - Purges jsDelivr CDN cache after publishing
15
15
  #
16
16
  # WHEN IT RUNS
@@ -30,14 +30,24 @@
30
30
  #
31
31
  # AUTOMATIC VERSION CLEANUP
32
32
  # -------------------------
33
- # After publishing a new version, this workflow automatically:
34
- # - Identifies all versions except the newly published one
35
- # - Unpublishes versions that are ≤72 hours old (npm policy allows this)
36
- # - Deprecates versions >72 hours old (unpublish no longer allowed)
37
- # - Result: Only the latest version remains active on npm
33
+ # After publishing, this workflow tidies up older versions — but only once the
34
+ # new version is actually readable on npm, and only with writes that change
35
+ # something. Both rules exist because of a real failure:
38
36
  #
39
- # Note: npm allows unpublishing versions within ~72 hours of publication.
40
- # After that window, versions can only be deprecated, not removed.
37
+ # - npm publish is asynchronous. The CLI exits 0 while the version is still
38
+ # being processed, and 1.30.30 only appeared in the registry 96 seconds later.
39
+ # - Unpublish and deprecate both work by rewriting the ENTIRE package document.
40
+ # A rewrite built from a snapshot taken before the new version landed deletes
41
+ # it again — which is how 1.30.30 vanished one second after it appeared,
42
+ # leaving `latest` at 1.30.20 while the GitHub release said 1.30.30.
43
+ #
44
+ # So:
45
+ # - Nothing touches the registry until GET /<pkg>/<version> succeeds
46
+ # - Versions that already carry a deprecation message are skipped entirely
47
+ # - The current version and every dist-tag target are never touched
48
+ # - Unpublishing is off (ALLOW_UNPUBLISH); npm rejects it from a granular token
49
+ # that bypasses 2FA, and each rejected attempt was still a document rewrite
50
+ # - The job fails if `latest` does not end up pointing at what it published
41
51
  #
42
52
  # VERSION BUMP RULE (push to main only)
43
53
  # -------------------------------------
@@ -144,6 +154,11 @@ env:
144
154
  PACKAGE_NAME_GH: 'KrX3D/TizenTube'
145
155
  NODE_VERSION: '24'
146
156
  UNPUBLISH_WINDOW_HOURS: '72'
157
+ # npm refuses `npm unpublish` from a granular access token that bypasses
158
+ # two-factor authentication, so every attempt was a 403 — and every 403 was
159
+ # still a full rewrite of the package document, which is how a freshly
160
+ # published version gets erased. Left off until the token can do it.
161
+ ALLOW_UNPUBLISH: 'false'
147
162
 
148
163
  jobs:
149
164
  build-publish-clean:
@@ -414,63 +429,137 @@ jobs:
414
429
  echo "Publishing package..."
415
430
  npm publish --access public
416
431
 
417
- - name: Wait for npm propagation (after publish)
432
+ # --------------------
433
+ # CONFIRM THE PUBLISH LANDED
434
+ # --------------------
435
+ # npm's publish endpoint is asynchronous. The CLI prints "Your package is
436
+ # being processed and may take a few minutes to become available" and exits
437
+ # 0 well before the version exists in the registry document.
438
+ #
439
+ # That is not a cosmetic delay, because every cleanup step below works by
440
+ # PUTting the WHOLE package document back. npm builds that document from
441
+ # whatever it can read at the time, so a PUT issued while the new version
442
+ # is still in flight writes a snapshot that does not contain it — and the
443
+ # version that was just published is gone again.
444
+ #
445
+ # 1.30.30 is what that looks like: published 18:40:16, appeared in the
446
+ # registry's own time map at 18:41:52, erased at 18:41:53 by the last
447
+ # npm deprecate of the very same job. The GitHub release said 1.30.30
448
+ # while the latest tag pointed back at 1.30.20, so every TV loading the
449
+ # userscript from the CDN got a bundle one version behind its own app.
450
+ #
451
+ # So nothing touches the registry until the new version is readable, and if
452
+ # it never becomes readable the job fails instead of cleaning up around a
453
+ # version that isn't there.
454
+ - name: Confirm the published version is live on npm
418
455
  if: steps.publish_npm.outcome == 'success'
456
+ id: confirm_publish
457
+ env:
458
+ PKG: ${{ env.PACKAGE_NAME }}
419
459
  run: |
420
- echo "Waiting for npm propagation..."
421
- sleep 10
460
+ set -euo pipefail
461
+ VER=$(node -e "console.log(require('./package.json').version)")
462
+ echo "$VER" > /tmp/current_version.txt
463
+ echo "Waiting for ${PKG}@${VER} to become readable..."
464
+ for i in $(seq 1 40); do
465
+ # The registry is read directly rather than through npm view, which
466
+ # has its own HTTP cache and will happily answer from a packument
467
+ # fetched before the publish landed.
468
+ if curl -fsSL "https://registry.npmjs.org/${PKG}/${VER}" -o /tmp/published.json; then
469
+ echo "${PKG}@${VER} is live (attempt ${i})."
470
+ echo "confirmed=true" >> "$GITHUB_OUTPUT"
471
+ exit 0
472
+ fi
473
+ sleep 15
474
+ done
475
+ echo "confirmed=false" >> "$GITHUB_OUTPUT"
476
+ echo "::error::${PKG}@${VER} never became readable on npm after 10 minutes. Cleanup is skipped so nothing can overwrite it."
477
+ exit 1
422
478
 
423
479
  # --------------------
424
- # FETCH VERSIONS
480
+ # FETCH THE REGISTRY DOCUMENT
425
481
  # --------------------
426
- - name: Gather versions and publish times
427
- if: steps.publish_npm.outcome == 'success'
482
+ - name: Gather versions, times and deprecation state
483
+ if: steps.confirm_publish.outputs.confirmed == 'true'
428
484
  env:
429
485
  PKG: ${{ env.PACKAGE_NAME }}
430
486
  run: |
431
487
  set -euo pipefail
432
- echo "Fetching versions & times for ${PKG} ..."
433
- npm view "${PKG}" versions --json > /tmp/versions.json
434
- npm view "${PKG}" time --json > /tmp/time.json
435
-
436
- CUR_VER=$(node -e "console.log(require('./package.json').version)")
437
- echo "Current repo version: $CUR_VER"
438
- echo "$CUR_VER" > /tmp/current_version.txt
488
+ echo "Fetching the registry document for ${PKG} ..."
489
+ curl -fsSL "https://registry.npmjs.org/${PKG}" -o /tmp/packument.json
490
+ node -e '
491
+ const fs = require("fs");
492
+ const doc = JSON.parse(fs.readFileSync("/tmp/packument.json","utf8"));
493
+ const versions = Object.keys(doc.versions || {});
494
+ const deprecated = versions.filter(function (v) { return doc.versions[v].deprecated; });
495
+ fs.writeFileSync("/tmp/versions.json", JSON.stringify(versions));
496
+ fs.writeFileSync("/tmp/time.json", JSON.stringify(doc.time || {}));
497
+ fs.writeFileSync("/tmp/deprecated.json", JSON.stringify(deprecated));
498
+ fs.writeFileSync("/tmp/dist_tags.json", JSON.stringify(doc["dist-tags"] || {}));
499
+ console.log("Live versions: " + versions.length + ", already deprecated: " + deprecated.length);
500
+ console.log("dist-tags: " + JSON.stringify(doc["dist-tags"] || {}));
501
+ '
502
+ echo "Current repo version: $(cat /tmp/current_version.txt)"
439
503
 
440
504
  # --------------------
441
505
  # DECIDE WHAT TO REMOVE
442
506
  # --------------------
507
+ # Two rules, both about never issuing a registry write that changes
508
+ # nothing — since each one rewrites the whole package document, and one of
509
+ # those rewrites is what erased 1.30.30:
510
+ #
511
+ # * a version that already carries a deprecation message is skipped. All
512
+ # 80 live versions were already deprecated, so the previous run made
513
+ # about 80 full-document writes for no effect at all.
514
+ #
515
+ # * unpublishing is off unless ALLOW_UNPUBLISH is "true". The npm token
516
+ # here is a granular access token, and npm refuses unpublish from tokens
517
+ # that bypass two-factor authentication ("Granular access tokens that
518
+ # bypass two-factor authentication may not perform this action"). Every
519
+ # attempt returned 403 — and every one of those was still a write.
520
+ # Re-enable it only with a token that can actually perform it.
521
+ #
522
+ # The version just published is kept, as before, and so is whatever each
523
+ # dist-tag resolves to: installing the tag npm points people at must not
524
+ # print a deprecation warning. The latest tag was itself deprecated by the
525
+ # run that lost 1.30.30.
443
526
  - name: Decide versions to unpublish / deprecate
444
- if: steps.publish_npm.outcome == 'success'
527
+ if: steps.confirm_publish.outputs.confirmed == 'true'
445
528
  env:
446
529
  UNPUBLISH_WINDOW_HOURS: ${{ env.UNPUBLISH_WINDOW_HOURS }}
530
+ ALLOW_UNPUBLISH: ${{ env.ALLOW_UNPUBLISH }}
447
531
  run: |
448
532
  set -euo pipefail
449
533
  node -e '
450
534
  const fs = require("fs");
451
535
  const versions = JSON.parse(fs.readFileSync("/tmp/versions.json","utf8")||"[]");
452
536
  const times = JSON.parse(fs.readFileSync("/tmp/time.json","utf8")||"{}");
537
+ const deprecated = new Set(JSON.parse(fs.readFileSync("/tmp/deprecated.json","utf8")||"[]"));
538
+ const distTags = JSON.parse(fs.readFileSync("/tmp/dist_tags.json","utf8")||"{}");
453
539
  const current = fs.readFileSync("/tmp/current_version.txt","utf8").trim();
454
540
 
455
541
  const windowHours = Number(process.env.UNPUBLISH_WINDOW_HOURS || 72);
542
+ const allowUnpublish = String(process.env.ALLOW_UNPUBLISH || "") === "true";
456
543
  const now = Date.now();
457
544
 
545
+ const keep = new Set([current].concat(Object.keys(distTags).map(function (tag) { return distTags[tag]; })));
546
+
458
547
  const toUnpublish = [];
459
548
  const toDeprecate = [];
460
549
 
461
550
  for (const v of versions) {
462
- if (v === current) continue;
551
+ if (keep.has(v)) continue;
463
552
  const t = times[v] ? new Date(times[v]) : null;
464
- if (!t) { toDeprecate.push(v); continue; }
465
- const ageHours = (now - t.getTime()) / 36e5;
466
- if (ageHours <= windowHours) toUnpublish.push(v);
467
- else toDeprecate.push(v);
553
+ const ageHours = t ? (now - t.getTime()) / 36e5 : Infinity;
554
+ if (allowUnpublish && t && ageHours <= windowHours) { toUnpublish.push(v); continue; }
555
+ if (!deprecated.has(v)) toDeprecate.push(v);
468
556
  }
469
557
 
470
558
  fs.writeFileSync("/tmp/to_unpublish.txt", toUnpublish.length ? toUnpublish.join("\n") + "\n" : "");
471
559
  fs.writeFileSync("/tmp/to_deprecate.txt", toDeprecate.length ? toDeprecate.join("\n") + "\n" : "");
472
- console.log("To unpublish (<= " + windowHours + "h):", toUnpublish);
473
- console.log("To deprecate (> " + windowHours + "h + unknown):", toDeprecate);
560
+ console.log("Keeping (current version + every dist-tag): " + Array.from(keep).join(", "));
561
+ console.log("To unpublish (<= " + windowHours + "h, enabled=" + allowUnpublish + "):", toUnpublish);
562
+ console.log("To deprecate (not already deprecated):", toDeprecate);
474
563
  '
475
564
 
476
565
  echo ""
@@ -485,7 +574,7 @@ jobs:
485
574
  # UNPUBLISH
486
575
  # --------------------
487
576
  - name: Unpublish old versions (verified) and fallback to deprecate
488
- if: steps.publish_npm.outcome == 'success'
577
+ if: steps.confirm_publish.outputs.confirmed == 'true'
489
578
  env:
490
579
  PKG: ${{ env.PACKAGE_NAME }}
491
580
  run: |
@@ -542,7 +631,7 @@ jobs:
542
631
  # DEPRECATE older (>72h)
543
632
  # --------------------
544
633
  - name: Deprecate older versions
545
- if: steps.publish_npm.outcome == 'success'
634
+ if: steps.confirm_publish.outputs.confirmed == 'true'
546
635
  env:
547
636
  PKG: ${{ env.PACKAGE_NAME }}
548
637
  run: |
@@ -563,13 +652,18 @@ jobs:
563
652
  # Final wait + verify final state
564
653
  # --------------------
565
654
  - name: Wait before final verify
566
- if: steps.publish_npm.outcome == 'success'
655
+ if: steps.confirm_publish.outputs.confirmed == 'true'
567
656
  run: |
568
657
  echo "Short wait to let registry propagate final state..."
569
658
  sleep 8
570
659
 
660
+ # The cleanup steps above rewrite the whole package document, so the last
661
+ # thing this job does is check that the version it published is still the
662
+ # one npm hands out. Without this the job reported success while `latest`
663
+ # had silently fallen back a release, and the first sign of it was a TV
664
+ # loading a userscript older than its own app.
571
665
  - name: Verify final state
572
- if: steps.publish_npm.outcome == 'success'
666
+ if: steps.confirm_publish.outputs.confirmed == 'true'
573
667
  env:
574
668
  PKG: ${{ env.PACKAGE_NAME }}
575
669
  run: |
@@ -577,14 +671,31 @@ jobs:
577
671
  echo "Final versions on npm:"
578
672
  npm view "${PKG}" versions --json
579
673
  echo ""
580
- echo "Latest version:"
581
- npm view "${PKG}" version
674
+
675
+ # Read through the registry rather than `npm view`, whose cache is
676
+ # what made the original breakage invisible here.
677
+ curl -fsSL "https://registry.npmjs.org/${PKG}" -o /tmp/packument_final.json
678
+ node -e '
679
+ const fs = require("fs");
680
+ const doc = JSON.parse(fs.readFileSync("/tmp/packument_final.json","utf8"));
681
+ const want = fs.readFileSync("/tmp/current_version.txt","utf8").trim();
682
+ const latest = (doc["dist-tags"] || {}).latest;
683
+ const live = !!(doc.versions || {})[want];
684
+ const deprecated = live && !!doc.versions[want].deprecated;
685
+ console.log("published: " + want + ", latest tag: " + latest + ", still live: " + live);
686
+ let failed = false;
687
+ if (!live) { console.log("::error::" + want + " is no longer on npm. A cleanup write removed the version this run published."); failed = true; }
688
+ if (latest !== want) { console.log("::error::npm latest is " + latest + ", not " + want + ". The CDN will serve the wrong bundle to every TV."); failed = true; }
689
+ if (deprecated) { console.log("::error::" + want + " was published deprecated."); failed = true; }
690
+ if (failed) process.exit(1);
691
+ console.log("npm latest matches the version this run published.");
692
+ '
582
693
 
583
694
  # --------------------
584
695
  # Purge jsDelivr cache
585
696
  # --------------------
586
697
  - name: Generate jsDelivr URLs to purge
587
- if: steps.publish_npm.outcome == 'success'
698
+ if: steps.confirm_publish.outputs.confirmed == 'true'
588
699
  id: cdn_urls
589
700
  run: |
590
701
  set -euo pipefail
@@ -636,7 +747,7 @@ jobs:
636
747
  printf '%s\n' "${URLS[@]}"
637
748
 
638
749
  - name: Purge jsDelivr cache
639
- if: steps.publish_npm.outcome == 'success'
750
+ if: steps.confirm_publish.outputs.confirmed == 'true'
640
751
  # jsDelivr rate-limits how often the same path can be re-purged
641
752
  # ("Purging request ... was throttled") — expected on frequent
642
753
  # releases, not an actual failure of the publish itself. The next
@@ -652,7 +763,7 @@ jobs:
652
763
  attempts: 3
653
764
 
654
765
  - name: Purge individual URLs (fallback if batch fails)
655
- if: steps.publish_npm.outcome == 'success' && failure()
766
+ if: steps.confirm_publish.outputs.confirmed == 'true' && failure()
656
767
  continue-on-error: true
657
768
  run: |
658
769
  set +e
@@ -698,6 +809,6 @@ jobs:
698
809
  echo "Individual purge attempts complete"
699
810
 
700
811
  - name: Cache purge complete
701
- if: steps.publish_npm.outcome == 'success'
812
+ if: steps.confirm_publish.outputs.confirmed == 'true'
702
813
 
703
814
  run: echo "✅ jsDelivr cache purge complete (npm + GitHub)"