Skip to content

Commit a79b385

Browse files
committed
chore: add Telegram release publishing
1 parent 60d3cd8 commit a79b385

7 files changed

Lines changed: 150 additions & 1 deletion

File tree

.env.release.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
TELEGRAM_BOT_TOKEN=
2+
TELEGRAM_RU_CHANNEL=@chitalka_reader_ru
3+
TELEGRAM_EN_CHANNEL=@chitalka_reader

AGENTS.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@
1515
- Replace the contents of `chitalka/demo` with the generated `dist/` output, preserving the demo repository's `.git` directory.
1616
- Commit and push the demo repository to its `gh-pages` branch.
1717
- Verify the live GitHub Pages URL loads the generated HTML, JavaScript, and CSS from the `/demo/` path.
18-
- Do not consider the release complete until both pushes and the live verification succeed.
18+
- Create a public GitHub Release from the matching `CHANGELOG.md` section and tag the release in both repositories.
19+
- Generate a new release image featuring the Chitalka mascot and visual details based on the actual release changelog. Store it under `release-assets/<version>/` and attach the same image to both Telegram posts.
20+
- Publish concise Release Notes in Russian to `@chitalka_reader_ru` and in English to `@chitalka_reader` after the GitHub Release and live demo are ready.
21+
- Read `TELEGRAM_BOT_TOKEN` only from `.env.release.local` or a protected CI secret. Never print, commit, or place the token in a command argument, changelog, release note, or task file.
22+
- Verify both Telegram posts and record their public links.
23+
- Do not consider the release complete until both repository pushes, both tags, the GitHub Release, live verification, and both Telegram posts succeed.

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,15 @@ The project uses TypeScript without a UI framework, [Vite](https://vite.dev/) fo
127127
## Releases and license
128128

129129
Release history is documented in [CHANGELOG.md](./CHANGELOG.md). Chitalka is maintained by [Oleg Mokhov](https://t.me/olegmokhov) and distributed under the [MIT License](./LICENSE).
130+
131+
Each public release also includes illustrated Release Notes in [the Russian Telegram channel](https://t.me/chitalka_reader_ru) and [the English Telegram channel](https://t.me/chitalka_reader). Release images and captions are stored under `release-assets/<version>/`.
132+
133+
Telegram publication uses `scripts/publish-telegram-release.mjs`. Copy `.env.release.example` to the ignored `.env.release.local`, add a newly issued bot token, and run a dry check before publishing:
134+
135+
```bash
136+
node scripts/publish-telegram-release.mjs \
137+
--channel @chitalka_reader_ru \
138+
--caption release-assets/2.01/telegram-ru.txt \
139+
--image release-assets/2.01/chitalka-2.01.png \
140+
--dry-run
141+
```
1.84 MB
Loading
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Chitalka 2.01
2+
3+
A major reading interface update:
4+
• consistent compact popovers for contents, settings, bookmarks, and quotes;
5+
• a calm dark theme with a restrained purple accent;
6+
• unified controls, typography, borders, hover, and keyboard focus;
7+
• book percentage, page number, and remaining time in the bottom bar;
8+
• interface language moved into settings;
9+
• a shorter quote-saving flow with an explicitly optional note.
10+
11+
We also fixed header hover after page turns, popover overlaps, and regressions in the two-page layout.
12+
13+
Read: https://chitalka.github.io/demo/
14+
Release: https://github.com/chitalka/reader/releases/tag/v2.01
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Читалка 2.01
2+
3+
Большое обновление интерфейса чтения:
4+
• единые компактные попапы для оглавления, настроек, закладок и цитат;
5+
• спокойная тёмная тема и аккуратный фиолетовый акцент;
6+
• одинаковые контролы, шрифты, обводки, hover и keyboard focus;
7+
• процент книги, номер страницы и оставшееся время в нижней панели;
8+
• язык интерфейса перенесён в настройки;
9+
• сохранение цитаты стало короче, а заметка явно обозначена как необязательная.
10+
11+
Исправили hover шапки после перелистывания, пересечения попапов и работу двухстраничного режима.
12+
13+
Читать: https://chitalka.github.io/demo/
14+
Релиз: https://github.com/chitalka/reader/releases/tag/v2.01
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { existsSync, readFileSync } from 'node:fs';
2+
import { basename, resolve } from 'node:path';
3+
4+
const TELEGRAM_CAPTION_LIMIT = 1024;
5+
const TELEGRAM_PHOTO_LIMIT = 10 * 1024 * 1024;
6+
7+
function fail(message) {
8+
console.error(message);
9+
process.exit(1);
10+
}
11+
12+
function loadReleaseEnvironment() {
13+
const filename = resolve('.env.release.local');
14+
if (!existsSync(filename)) return;
15+
16+
for (const rawLine of readFileSync(filename, 'utf8').split(/\r?\n/u)) {
17+
const line = rawLine.trim();
18+
if (!line || line.startsWith('#')) continue;
19+
const separator = line.indexOf('=');
20+
if (separator < 1) continue;
21+
const key = line.slice(0, separator).trim();
22+
let value = line.slice(separator + 1).trim();
23+
if (
24+
value.length >= 2
25+
&& ((value.startsWith('"') && value.endsWith('"'))
26+
|| (value.startsWith("'") && value.endsWith("'")))
27+
) value = value.slice(1, -1);
28+
if (!(key in process.env)) process.env[key] = value;
29+
}
30+
}
31+
32+
function parseArguments(argv) {
33+
const options = { dryRun: false };
34+
for (let index = 0; index < argv.length; index += 1) {
35+
const argument = argv[index];
36+
if (argument === '--dry-run') {
37+
options.dryRun = true;
38+
continue;
39+
}
40+
if (!argument.startsWith('--')) fail(`Unexpected argument: ${argument}`);
41+
const value = argv[index + 1];
42+
if (!value || value.startsWith('--')) fail(`Missing value for ${argument}`);
43+
options[argument.slice(2)] = value;
44+
index += 1;
45+
}
46+
return options;
47+
}
48+
49+
loadReleaseEnvironment();
50+
const options = parseArguments(process.argv.slice(2));
51+
const channel = options.channel;
52+
const captionFile = options.caption;
53+
const imageFile = options.image;
54+
55+
if (!channel || !captionFile || !imageFile) {
56+
fail('Usage: node scripts/publish-telegram-release.mjs --channel @channel --caption notes.txt --image release.png [--dry-run]');
57+
}
58+
59+
const captionPath = resolve(captionFile);
60+
const imagePath = resolve(imageFile);
61+
if (!existsSync(captionPath)) fail(`Caption file not found: ${captionFile}`);
62+
if (!existsSync(imagePath)) fail(`Image file not found: ${imageFile}`);
63+
64+
const caption = readFileSync(captionPath, 'utf8').trim();
65+
const image = readFileSync(imagePath);
66+
if (!caption) fail('Caption must not be empty');
67+
if (caption.length > TELEGRAM_CAPTION_LIMIT) {
68+
fail(`Caption is ${caption.length} characters; Telegram allows ${TELEGRAM_CAPTION_LIMIT}`);
69+
}
70+
if (image.length > TELEGRAM_PHOTO_LIMIT) {
71+
fail(`Image is ${image.length} bytes; Telegram sendPhoto allows ${TELEGRAM_PHOTO_LIMIT}`);
72+
}
73+
74+
if (options.dryRun) {
75+
console.log(JSON.stringify({
76+
channel,
77+
captionCharacters: caption.length,
78+
imageBytes: image.length,
79+
}));
80+
process.exit(0);
81+
}
82+
83+
const token = process.env.TELEGRAM_BOT_TOKEN;
84+
if (!token) fail('TELEGRAM_BOT_TOKEN is missing from .env.release.local');
85+
86+
const form = new FormData();
87+
form.set('chat_id', channel);
88+
form.set('caption', caption);
89+
form.set('photo', new Blob([image], { type: 'image/png' }), basename(imagePath));
90+
91+
const response = await fetch(`https://api.telegram.org/bot${token}/sendPhoto`, {
92+
method: 'POST',
93+
body: form,
94+
});
95+
const payload = await response.json();
96+
if (!response.ok || !payload.ok) {
97+
fail(`Telegram publication failed: ${payload.description || response.statusText}`);
98+
}
99+
100+
const channelName = channel.replace(/^@/u, '');
101+
console.log(`https://t.me/${channelName}/${payload.result.message_id}`);

0 commit comments

Comments
 (0)