name: Notify Telegram # tauri-action creates the release with the default GITHUB_TOKEN, and GitHub # Actions deliberately suppresses `release: published` events for releases # made by GITHUB_TOKEN (to prevent recursive workflow chains). So we can't # listen for `release: published` — it will never fire on stable releases. # # Instead, chain off the Release workflow via `workflow_run`, the same way # `publish-repos.yml` does. `workflow_dispatch` is kept so a missed # announcement can be replayed by hand. on: workflow_dispatch: inputs: tag: description: "Release tag to announce (e.g. v0.23.0). Leave empty for latest stable." required: false type: string workflow_run: workflows: ["Release"] types: - completed permissions: contents: read models: read jobs: notify: if: > github.repository == 'zhom/donutbrowser' && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: main fetch-depth: 0 - name: Resolve release tag id: tag env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} INPUT_TAG: ${{ inputs.tag }} # `head_branch` of a workflow_run trigger is attacker-influenceable # (anyone with push to a tag can choose its name), so we pass it via # env and validate before use rather than splicing it into the # shell script literally. See CodeQL actions/code-injection. EVENT_NAME: ${{ github.event_name }} WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} REPO: ${{ github.repository }} run: | if [[ -n "${INPUT_TAG:-}" ]]; then TAG="${INPUT_TAG}" elif [[ "${EVENT_NAME}" == "workflow_run" ]]; then # The Release workflow runs on `push: tags: v*` so head_branch # of the triggering run is the tag name. Reject anything that # isn't a plain tag-shaped string to keep this resistant to # shell metacharacters injected via a crafted ref name. if [[ ! "${WORKFLOW_RUN_HEAD_BRANCH}" =~ ^[A-Za-z0-9._/-]+$ ]]; then echo "::error::Refusing tag with unexpected characters: ${WORKFLOW_RUN_HEAD_BRANCH}" exit 1 fi TAG="${WORKFLOW_RUN_HEAD_BRANCH}" else TAG=$(gh release view --repo "${REPO}" --json tagName -q .tagName) fi echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "Resolved tag: ${TAG}" - name: Skip pre-releases / missing releases id: gate env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.tag.outputs.tag }} run: | # Tag like `nightly-…` or `nightly` is never an announceable # stable release. Short-circuit before hitting the API. if [[ "${TAG}" == nightly* ]]; then echo "Tag '${TAG}' is a rolling/nightly build, skipping Telegram post." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi # Only stable semver tags vX.Y.Z are eligible. Reject anything # with a pre-release suffix (`-rc1`, `-beta`, etc.). if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Tag '${TAG}' is not a stable semver tag, skipping." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi # Confirm the release exists and isn't marked prerelease in the # GitHub UI — guards against someone manually flipping the flag. RELEASE_JSON=$(gh release view "${TAG}" --repo "${{ github.repository }}" --json isPrerelease,tagName 2>/dev/null || echo "") if [[ -z "${RELEASE_JSON}" ]]; then echo "Release ${TAG} not found via gh — skipping." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi IS_PRE=$(jq -r .isPrerelease <<< "${RELEASE_JSON}") if [[ "${IS_PRE}" == "true" ]]; then echo "Release ${TAG} is marked prerelease, skipping." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi echo "skip=false" >> "$GITHUB_OUTPUT" - name: Collect commits between previous tag and current tag id: commits if: steps.gate.outputs.skip != 'true' env: TAG: ${{ steps.tag.outputs.tag }} run: | PREV_TAG=$(git tag --sort=-version:refname \ | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ | grep -v "^${TAG}$" \ | head -n 1) if [ -z "$PREV_TAG" ]; then PREV_TAG=$(git rev-list --max-parents=0 HEAD) fi git log --pretty=format:"- %s (%h)" "${PREV_TAG}..${TAG}" --no-merges > commits.txt echo "previous-tag=${PREV_TAG}" >> "$GITHUB_OUTPUT" echo "Collected $(wc -l < commits.txt) commits between ${PREV_TAG} and ${TAG}." - name: Generate summary with AI id: ai if: steps.gate.outputs.skip != 'true' uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0 with: prompt-file: .github/prompts/telegram-release-summary.prompt.yml input: | version: ${{ steps.tag.outputs.tag }} file_input: | commits: ./commits.txt max-tokens: 1024 - name: Post release announcement to Telegram if: steps.gate.outputs.skip != 'true' env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} TAG: ${{ steps.tag.outputs.tag }} REPO: ${{ github.repository }} AI_RESPONSE_FILE: ${{ steps.ai.outputs.response-file }} AI_RESPONSE: ${{ steps.ai.outputs.response }} run: | if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then echo "::warning::TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID is not set — skipping Telegram notification." exit 0 fi # Prefer the file output — `response` can be truncated for longer summaries. if [ -n "$AI_RESPONSE_FILE" ] && [ -f "$AI_RESPONSE_FILE" ]; then SUMMARY=$(cat "$AI_RESPONSE_FILE") else SUMMARY="$AI_RESPONSE" fi if [ -z "${SUMMARY//[[:space:]]/}" ]; then echo "::error::AI summary is empty" exit 1 fi # HTML-escape the AI summary before injecting into Telegram HTML mode — # commit messages can legitimately contain `<`, `>`, `&` and the AI may echo them. ESCAPED_CHANGES=$(printf '%s' "$SUMMARY" \ | python3 -c "import html, sys; sys.stdout.write(html.escape(sys.stdin.read()))") VERSION="${TAG}" VERSION_NUM="${TAG#v}" RELEASE_URL="https://github.com/${REPO}/releases/tag/${VERSION}" DL="https://github.com/${REPO}/releases/download/${VERSION}" # Build the API payload in one jq pass — keeps every literal # newline, every angle bracket, and every quote correctly escaped # for both shell and JSON. PAYLOAD=$(jq -n \ --arg chat_id "$TELEGRAM_CHAT_ID" \ --arg version "$VERSION" \ --arg changes "$ESCAPED_CHANGES" \ --arg dl "$DL" \ --arg vnum "$VERSION_NUM" \ --arg release_url "$RELEASE_URL" \ '{ chat_id: $chat_id, parse_mode: "HTML", disable_web_page_preview: true, text: ( "Donut Browser " + $version + " released\n\n" + $changes + "\n" + "Download\n" + "macOS (Apple Silicon) · " + "macOS (Intel)\n" + "Windows x64 · " + "Linux x64\n\n" + "Full release notes" ) }') # Use --fail-with-body so we surface Telegram's error JSON on 4xx/5xx # instead of just a curl exit code. RESPONSE=$(curl -sSL --fail-with-body \ -H "Content-Type: application/json" \ -d "$PAYLOAD" \ "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage") \ || { echo "::error::Telegram API call failed"; echo "$RESPONSE"; exit 1; } if [ "$(jq -r .ok <<< "$RESPONSE")" != "true" ]; then echo "::error::Telegram API rejected the message:" jq . <<< "$RESPONSE" exit 1 fi echo "Posted to Telegram (message_id $(jq -r .result.message_id <<< "$RESPONSE"))"