Files
eranos/scripts/extract-release-notes.mjs
T
Chad Curtis 1b4399df68 Rebrand app identifier and IPA name from Ditto to Agora
Renames the Capacitor app identifier from pub.agora.app to
spot.agora.app and cleans up Ditto-branded artifacts that don't refer
to upstream Ditto-the-project or Ditto-stack services.

App identifier (pub.agora.app -> spot.agora.app):
- capacitor.config.ts appId
- android applicationId, namespace, package_name string, custom_url_scheme
- iOS PRODUCT_BUNDLE_IDENTIFIER (Debug + Release)
- public/.well-known/assetlinks.json package_name
- public/.well-known/apple-app-site-association app id
- Info.plist BGTaskSchedulerPermittedIdentifiers and the matching
  Swift bgTaskIdentifier (previously mismatched: plist said
  pub.agora.app.notification-refresh, Swift said
  pub.ditto.app.notification-refresh, so background refresh would
  silently fail to register)
- src/lib/helpContent.ts Zapstore URLs
- .gitlab-ci.yml --package_name for fastlane supply

Android Java package (pub.ditto.app -> spot.agora.app):
- Move android/app/src/main/java/pub/ditto/app/ ->
  android/app/src/main/java/spot/agora/app/ (4 files: MainActivity,
  DittoNotificationPlugin, NostrPoller, NotificationRelayService)
- Update package declarations to match the new Android namespace
  (was a hard build failure with namespace = spot.agora.app)
- Update proguard -keep rule
- Update NotificationRelayService ACTION_FETCH intent string
  pub.ditto.app.ACTION_FETCH -> spot.agora.app.ACTION_FETCH

Fastlane (pub.ditto.app -> spot.agora.app):
- Appfile, Matchfile, Fastfile provisioning profile specifiers.
  Matchfile still points at Soapbox's certificates git repo; a new
  match repo with certs for spot.agora.app is required before iOS CI
  signing works.

IPA artifact name (Ditto.ipa -> Agora.ipa):
- Fastfile output_name and matching CI artifact paths
- .gitlab-ci.yml: artifacts/Ditto.ipa references and the GitLab
  Generic Packages path from /packages/generic/ditto/ ->
  /packages/generic/agora/ (matches how APK/AAB are already
  published). Existing release artifacts at the old path remain
  reachable; new releases land at the new path.

Release-notes script fallback (Ditto vX.Y.Z -> Agora vX.Y.Z):
- scripts/extract-release-notes.mjs fallback used as the App Store /
  Play Store 'What's New' blurb when a changelog section has no
  summary.

manifest.webmanifest:
- Update related_applications Play Store entry to spot.agora.app.
- Remove the iTunes related_applications entry that pointed at
  the existing Ditto App Store listing; not applicable to Agora
  until Agora has its own listing.

Capacitor sync incidentals:
- npm run cap:sync picked up @capacitor/barcode-scanner registration
  that had been missed in a prior plugin install
  (android/app/capacitor.build.gradle, capacitor.settings.gradle,
  ios/App/CapApp-SPM/Package.swift).

Intentionally NOT touched:
- ditto.json filename, DittoConfigSchema, DittoConfig, and JSDoc
  references to ditto.json. The config-system shape is shared with
  upstream Ditto by design.
- relay.ditto.pub, blossom.ditto.pub, ditto.pub/api/* and other
  Ditto-stack services Agora actively consumes.
- The DittoNotificationPlugin Android/iOS class name, the
  DittoNotification JS bridge name, ditto_notification_config
  SharedPreferences keys, ic_stat_ditto drawables, and the
  DittoBridgeViewController. Renaming requires a coordinated
  JS-side rename plus a SharedPreferences migration or existing
  users on the Ditto fork lose their notification config on upgrade.
- Ditto references in skill docs, NIP.md kind comments, README, and
  zapstore.yaml attribution \u2014 those correctly describe the upstream
  Ditto project that Agora forked from.

Follow-ups required before CI succeeds end-to-end (out of scope here):
- Stand up a new fastlane match git repo containing certs +
  provisioning profiles for spot.agora.app, or update Matchfile
  git_url to point at it.
- Register spot.agora.app in App Store Connect for team GZLTTH5DLM
  and create a new App Store listing.
- Create a new Google Play Console listing for spot.agora.app
  (package name is immutable per app on Play; the existing
  pub.agora.app listing cannot be reused).
- Re-publish to Zapstore under spot.agora.app so the URLs in
  helpContent.ts resolve.
2026-05-17 18:42:46 -05:00

142 lines
4.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Extract release notes from CHANGELOG.md for a given version.
*
* The CHANGELOG follows Keep a Changelog format with one extension: each release
* section MAY begin with a single plaintext paragraph (the "summary") before any
* `### Added` / `### Changed` / etc. heading. The summary is used as the release
* blurb on the App Store, Play Store, and the in-app version-update toast. The
* full section body is used as the GitLab Release description.
*
* Format:
*
* ## [X.Y.Z] - YYYY-MM-DD
*
* A short single-paragraph summary (max 500 characters by convention).
*
* ### Added
* - bullet
* - bullet
*
* ### Changed
* - bullet
*
* Usage:
* node scripts/extract-release-notes.mjs <version> [--summary] [--changelog <path>]
*
* --summary Print only the summary paragraph (no headings, no bullets).
* Falls back to "Agora vX.Y.Z" if the section has no summary.
* --changelog Path to the changelog file. Defaults to CHANGELOG.md.
*
* Exits 0 with the extracted text on stdout. Exits non-zero if the version is
* not found in the changelog.
*/
import { readFileSync } from 'node:fs';
import { argv, exit, stderr, stdout } from 'node:process';
function parseArgs(args) {
let version;
let summary = false;
let changelog = 'CHANGELOG.md';
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--summary') summary = true;
else if (arg === '--changelog') changelog = args[++i];
else if (!arg.startsWith('--') && !version) version = arg;
else {
stderr.write(`Unknown argument: ${arg}\n`);
exit(2);
}
}
if (!version) {
stderr.write('Usage: extract-release-notes.mjs <version> [--summary] [--changelog <path>]\n');
exit(2);
}
// Strip a leading "v" so callers can pass either "v2.14.3" or "2.14.3".
if (version.startsWith('v')) version = version.slice(1);
return { version, summary, changelog };
}
/**
* Extract the lines belonging to a single version section from changelog text,
* not including the version heading itself.
*/
function extractSection(markdown, version) {
const lines = markdown.split('\n');
const headingPattern = new RegExp(
`^## \\[${version.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\]`,
);
const nextHeadingPattern = /^## \[/;
let inSection = false;
const out = [];
for (const line of lines) {
if (!inSection) {
if (headingPattern.test(line)) {
inSection = true;
continue;
}
} else {
if (nextHeadingPattern.test(line)) break;
out.push(line);
}
}
return inSection ? out : null;
}
/**
* Pull the leading non-blank paragraph from a section, stopping at the first
* `###` category heading or `-` bullet. Returns null if no summary paragraph.
*/
function extractSummary(sectionLines) {
const paragraph = [];
let started = false;
for (const line of sectionLines) {
const trimmed = line.trim();
if (!started) {
if (!trimmed) continue;
// If the very first non-blank line is a heading or bullet, there's no summary.
if (trimmed.startsWith('#') || trimmed.startsWith('- ')) return null;
started = true;
paragraph.push(trimmed);
continue;
}
// We're inside the paragraph. A blank line, a heading, or a bullet ends it.
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('- ')) break;
paragraph.push(trimmed);
}
return paragraph.length ? paragraph.join(' ') : null;
}
/** Trim leading and trailing blank lines from a list of lines. */
function trimBlankEdges(lines) {
let start = 0;
let end = lines.length;
while (start < end && !lines[start].trim()) start++;
while (end > start && !lines[end - 1].trim()) end--;
return lines.slice(start, end);
}
const { version, summary, changelog } = parseArgs(argv.slice(2));
const markdown = readFileSync(changelog, 'utf8');
const section = extractSection(markdown, version);
if (!section) {
stderr.write(`Version ${version} not found in ${changelog}\n`);
exit(1);
}
if (summary) {
const text = extractSummary(section);
stdout.write(text ?? `Agora v${version}`);
stdout.write('\n');
} else {
const body = trimBlankEdges(section).join('\n');
if (body) {
stdout.write(body);
stdout.write('\n');
} else {
stdout.write(`Agora v${version}\n`);
}
}