Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/en/release-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ In repository **Settings → Secrets and variables → Actions**, configure:
| --- | --- | --- |
| Secret | `OPENAI_API_KEY` | OpenAI API key, used only for generating EN/RU GitHub and F-Droid notes. |
| Variable / Secret | `OPENAI_RELEASE_MODEL` | A model available to your API project that supports Responses API Structured Outputs, for example `gpt-6.1-sol`. Variables take priority over Secrets if both are set. No implicit model fallback. |
| Secret | `RELEASE_BOT_TOKEN` | Fine-grained PAT limited to this repository: Contents read/write, Pull requests read/write, Actions read. |
| Secret | `RELEASE_BOT_TOKEN` | Fine-grained PAT limited to this repository: Contents read/write, Workflows read/write, Pull requests read/write, Actions read. |

Tag creation through the Git references API can also require Workflows write permission,
including when tagging an earlier commit after `main` advances. For a classic PAT, use
`repo` and `workflow` scopes. Check the token in GitHub **Settings → Developer settings →
Personal access tokens**; after replacing it, update the repository's `RELEASE_BOT_TOKEN`
secret. The helper reports the failed API method, endpoint and HTTP status without dumping
response bodies. HTTP 403 on `POST .../git/refs` requires checking these permissions and
repository tag rules; a generic API error alone does not establish the cause.
[GitHub reference API permissions](https://docs.github.com/en/rest/git/refs#create-a-reference).

The tag workflow also requires `VIRUSTOTAL_API_KEY`. After signing, it submits all
five public release APKs to VirusTotal, including the universal APK via the large-file
Expand Down
11 changes: 10 additions & 1 deletion docs/ru/release-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ workflow нужно явно запустить с новой версией.
| --- | --- | --- |
| Secret | `OPENAI_API_KEY` | Ключ OpenAI API, только для генерации EN/RU текстов GitHub и F-Droid. |
| Variable / Secret | `OPENAI_RELEASE_MODEL` | Доступная вашему API-проекту модель с Responses API Structured Outputs, например `gpt-6.1-sol`. Если заданы оба варианта, Variables имеют приоритет над Secrets. Неявного выбора другой модели нет. |
| Secret | `RELEASE_BOT_TOKEN` | Fine-grained PAT только для этого репозитория: Contents read/write, Pull requests read/write, Actions read. |
| Secret | `RELEASE_BOT_TOKEN` | Fine-grained PAT только для этого репозитория: Contents read/write, Workflows read/write, Pull requests read/write, Actions read. |

Создание тега через Git references API также может требовать Workflows write,
в том числе при тегировании предыдущего commit после продвижения `main`. Для classic PAT
нужны scope `repo` и `workflow`. Проверьте токен в GitHub **Settings → Developer settings →
Personal access tokens**; при замене обновите секрет репозитория `RELEASE_BOT_TOKEN`.
Скрипт показывает метод, endpoint и HTTP-статус сбоя без вывода тела ответа.
HTTP 403 на `POST .../git/refs` требует проверки этих прав и правил тегов репозитория;
общая ошибка API сама по себе не доказывает причину.
[Права GitHub reference API](https://docs.github.com/en/rest/git/refs#create-a-reference).

Workflow по тегу также требует `VIRUSTOTAL_API_KEY`. После подписи все пять публичных
релизных APK отправляются в VirusTotal; для universal используется загрузка больших
Expand Down
20 changes: 16 additions & 4 deletions scripts/release_automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,22 @@ def command(*args, input_text=None):
result = subprocess.run(
args, input=input_text, capture_output=True, text=True, timeout=60, env=env
)
require(
result.returncode == 0,
f"{args[0]} {args[1]} failed; inspect the remote state before retrying",
)
if result.returncode:
operation = f"{args[0]} {args[1]}"
hint = "inspect the remote state before retrying"
if args[:2] == ("gh", "api"):
method = args[args.index("--method") + 1]
operation = f"GitHub API {method} {args[2].split('?')[0]}"
status = re.search(r"\bHTTP ([1-5][0-9]{2})\b", result.stderr)
if status:
operation += f" (HTTP {status[1]})"
if status and status[1] == "403" and args[2].endswith("/git/refs"):
hint = (
"check RELEASE_BOT_TOKEN Contents/Workflows write permissions "
"and repository tag rules"
)
# API responses and subprocess output may contain credentials or release evidence.
raise RuntimeError(f"{operation} failed; {hint}")
return result.stdout.strip()


Expand Down
32 changes: 32 additions & 0 deletions scripts/tests/test_release_automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import unittest
import urllib.request
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch

sys.path.insert(0, str(Path(__file__).parents[1]))
Expand Down Expand Up @@ -52,6 +53,37 @@ def test_api_mutations_use_explicit_http_method_and_structured_stdin(self):
input_text=json.dumps({"ref": "refs/tags/v1.0.0", "sha": "a" * 40}),
)

def test_api_failure_identifies_request_and_status_without_leaking_output(self):
for status in ("403", "422", None):
with (
self.subTest(status=status),
patch.dict(
os.environ, {"GITHUB_REPOSITORY": "owner/repo", "GH_DEBUG": "api"}
),
patch.object(
m.subprocess,
"run",
return_value=SimpleNamespace(
returncode=1,
stdout="synthetic-secret-response",
stderr="synthetic-secret-token "
+ (f"(HTTP {status})" if status else "network failure"),
),
) as run,
):
with self.assertRaises(RuntimeError) as error:
m.api("git/refs", {"ref": "refs/tags/v1.2.3", "sha": "a" * 40})
message = str(error.exception)
self.assertIn("GitHub API POST repos/owner/repo/git/refs", message)
self.assertNotIn("synthetic-secret", message)
self.assertNotIn("GH_DEBUG", run.call_args.kwargs["env"])
if status:
self.assertIn(f"HTTP {status}", message)
if status == "403":
self.assertIn("Contents/Workflows write", message)
else:
self.assertNotIn("Contents/Workflows write", message)

def test_version_increment_preserves_fdroid_abi_layout(self):
updated, code = m.bump_version(SOURCE, "0.1.2")
self.assertEqual(15000, code)
Expand Down
Loading