fix(release): public links in release notes and a cancelled status
ci / ts / ts (packages + web) (push) Successful in 1m24s
release / release (версия и заметки) (push) Successful in 45s
release / android (APK в релиз) (push) Failing after 12m0s

The job reaches the Gitea API at its internal address, so html_url in the
response pointed at http://10.10.1.129:3000 and every link in the notes was
unreachable from outside. Build links from PUBLIC_URL (repo variable,
defaults to https://gitea.hsrv.site). A cancelled Android build now shows
"cancelled" instead of staying at "building".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 05:09:59 +03:00
1 parent 718125b719
commit a239650075
4 files changed
+30 -5

No files matched your search

+9
View File
@@ -5,6 +5,8 @@
# #
# Секреты (Settings → Actions → Secrets): # Секреты (Settings → Actions → Secrets):
# RELEASE_TOKEN (необязательно) — токен для релиза; без него — встроенный токен job (права на запись в репозиторий) # RELEASE_TOKEN (необязательно) — токен для релиза; без него — встроенный токен job (права на запись в репозиторий)
# Переменные (Settings → Actions → Variables):
# PUBLIC_URL (необязательно) — внешний адрес Gitea для ссылок в тексте релиза; по умолчанию https://gitea.hsrv.site
# ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD — подпись APK # ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD — подпись APK
name: release name: release
@@ -37,6 +39,7 @@ jobs:
env: env:
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
PUBLIC_URL: ${{ vars.PUBLIC_URL || 'https://gitea.hsrv.site' }}
TAG: ${{ github.ref_name }} TAG: ${{ github.ref_name }}
VERSION: ${{ steps.v.outputs.PIPU_VERSION }} VERSION: ${{ steps.v.outputs.PIPU_VERSION }}
PRERELEASE: ${{ steps.v.outputs.PIPU_PRERELEASE }} PRERELEASE: ${{ steps.v.outputs.PIPU_PRERELEASE }}
@@ -60,6 +63,7 @@ jobs:
# для scripts/release.mjs # для scripts/release.mjs
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
PUBLIC_URL: ${{ vars.PUBLIC_URL || 'https://gitea.hsrv.site' }}
TAG: ${{ github.ref_name }} TAG: ${{ github.ref_name }}
VERSION: ${{ needs.release.outputs.version }} VERSION: ${{ needs.release.outputs.version }}
VERSION_CODE: ${{ needs.release.outputs.version_code }} VERSION_CODE: ${{ needs.release.outputs.version_code }}
@@ -141,3 +145,8 @@ jobs:
- name: Статус в релизе — сборка не удалась - name: Статус в релизе — сборка не удалась
if: failure() if: failure()
run: node scripts/release.mjs android failure run: node scripts/release.mjs android failure
# срабатывает при отмене из интерфейса; если раннер убит целиком, статус обновит следующий запуск
- name: Статус в релизе — сборка отменена
if: cancelled()
run: node scripts/release.mjs android cancelled
+4
View File
@@ -106,6 +106,10 @@ container:
| `RELEASE_TOKEN` | Необязательно. Создание и редактирование релиза, загрузка ассетов (scope `write:repository`). Без него используется встроенный токен job (`GITHUB_TOKEN`), у которого есть права на запись в этот репозиторий | | `RELEASE_TOKEN` | Необязательно. Создание и редактирование релиза, загрузка ассетов (scope `write:repository`). Без него используется встроенный токен job (`GITHUB_TOKEN`), у которого есть права на запись в этот репозиторий |
| `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | Подпись APK. **Ключ один на всю жизнь приложения** — хранить резервную копию вне Gitea. Без них финальный релиз падает; `-rc` собирается с отладочной подписью. | | `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | Подпись APK. **Ключ один на всю жизнь приложения** — хранить резервную копию вне Gitea. Без них финальный релиз падает; `-rc` собирается с отладочной подписью. |
Переменная репозитория `PUBLIC_URL` (Settings → Actions → Variables) — внешний адрес Gitea для ссылок
в тексте релиза, по умолчанию `https://gitea.hsrv.site`. Нужна потому, что job обращается к API по внутреннему
адресу, и ссылки из ответа API ведут туда же.
## Деплой ## Деплой
Автодеплой не обязателен. На сервере в клоне репозитория: `deploy/update.sh [vX.Y.Z]` — checkout тега, Автодеплой не обязателен. На сервере в клоне репозитория: `deploy/update.sh [vX.Y.Z]` — checkout тега,
+16 -5
View File
@@ -2,10 +2,12 @@
// Релиз в Gitea (docs/ci-cd.md#релиз): сначала создаётся релиз с заметками, потом сборка Android // Релиз в Gitea (docs/ci-cd.md#релиз): сначала создаётся релиз с заметками, потом сборка Android
// докладывает в него APK и переписывает блок статуса в тексте. // докладывает в него APK и переписывает блок статуса в тексте.
// node scripts/release.mjs create — создать релиз по тегу (при перезапуске — обновить) // node scripts/release.mjs create — создать релиз по тегу (при перезапуске — обновить)
// node scripts/release.mjs android building|failure — статус сборки APK в тексте релиза // node scripts/release.mjs android building|failure|cancelled — статус сборки APK в тексте релиза
// node scripts/release.mjs android success <file.apk> — приложить APK и .sha256, статус «собран» // node scripts/release.mjs android success <file.apk> — приложить APK и .sha256, статус «собран»
// Окружение: GITEA_API (<server>/api/v1/repos/<owner>/<repo>), GITEA_TOKEN, TAG, VERSION, VERSION_CODE, // Окружение: GITEA_API (<server>/api/v1/repos/<owner>/<repo>), GITEA_TOKEN, TAG, VERSION, VERSION_CODE,
// PRERELEASE (true|false), GITHUB_RUN_ID; для success — SIGNING (release|debug). // PRERELEASE (true|false), GITHUB_RUN_ID; для success — SIGNING (release|debug).
// PUBLIC_URL — внешний адрес Gitea для ссылок в тексте (job ходит в API по внутреннему адресу, и html_url
// из ответа указывает на него же).
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
@@ -57,7 +59,12 @@ export function renderAndroidStatus({ status, runUrl, versionCode, apk, signing
? ['⚠️ Подписан отладочным ключом: не установится поверх версии с релизной подписью.'] ? ['⚠️ Подписан отладочным ключом: не установится поверх версии с релизной подписью.']
: []), : []),
] ]
: status === 'failure' : status === 'cancelled'
? [
`⏹ **Android:** сборка APK отменена — [лог сборки](${runUrl}).`,
'APK появится здесь после перезапуска сборки.',
]
: status === 'failure'
? [ ? [
`❌ **Android:** сборка APK не удалась — [лог сборки](${runUrl}).`, `❌ **Android:** сборка APK не удалась — [лог сборки](${runUrl}).`,
'Серверная часть релиза от этого не зависит; APK появится здесь после перезапуска сборки.', 'Серверная часть релиза от этого не зависит; APK появится здесь после перезапуска сборки.',
@@ -182,9 +189,11 @@ function commitsBetween(prevTag, tag) {
async function context() { async function context() {
const repo = await api('GET', ''); const repo = await api('GET', '');
const publicUrl = process.env.PUBLIC_URL?.replace(/\/+$/, '');
const repoUrl = publicUrl ? `${publicUrl}/${repo.full_name}` : repo.html_url;
const tag = env('TAG'); const tag = env('TAG');
const release = await api('GET', `/releases/tags/${encodeURIComponent(tag)}`); const release = await api('GET', `/releases/tags/${encodeURIComponent(tag)}`);
return { repoUrl: repo.html_url, tag, release, runUrl: `${repo.html_url}/actions/runs/${env('GITHUB_RUN_ID')}` }; return { repoUrl, tag, release, runUrl: `${repoUrl}/actions/runs/${env('GITHUB_RUN_ID')}` };
} }
async function create() { async function create() {
@@ -242,8 +251,10 @@ async function android(status, apkPath) {
async function main() { async function main() {
const [cmd, ...args] = process.argv.slice(2); const [cmd, ...args] = process.argv.slice(2);
if (cmd === 'create') return create(); if (cmd === 'create') return create();
if (cmd === 'android' && ['building', 'success', 'failure'].includes(args[0])) return android(args[0], args[1]); if (cmd === 'android' && ['building', 'success', 'failure', 'cancelled'].includes(args[0])) {
throw new Error('использование: release.mjs create | android building|failure | android success <file.apk>'); return android(args[0], args[1]);
}
throw new Error('использование: release.mjs create | android building|failure|cancelled | android success <file.apk>');
} }
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
+1
View File
@@ -59,6 +59,7 @@ test('android status blocks', () => {
assert.match(debug, /отладочным ключом/); assert.match(debug, /отладочным ключом/);
assert.match(renderAndroidStatus({ status: 'failure', runUrl: 'https://run' }), /не удалась — \[лог сборки\]/); assert.match(renderAndroidStatus({ status: 'failure', runUrl: 'https://run' }), /не удалась — \[лог сборки\]/);
assert.match(renderAndroidStatus({ status: 'cancelled', runUrl: 'https://run' }), /отменена — \[лог сборки\]/);
}); });
test('replaces only the status block and is idempotent', () => { test('replaces only the status block and is idempotent', () => {