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 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: 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 }} 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 # Find the previous stable tag (skip the current one) so the # changelog range is well-defined. 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 strip_prefix() { echo "$1" | sed -E 's/^[a-z]+(\([^)]*\))?: //'; } # Build a plain bullet list from feat / fix / refactor commits. # Other commit types (chore, docs, ci, test, deps) are intentionally # filtered out to keep the channel focused on user-visible changes. CHANGES="" while IFS= read -r msg; do [ -z "$msg" ] && continue case "$msg" in feat\(*\):*|feat:*|fix\(*\):*|fix:*|refactor\(*\):*|refactor:*) CHANGES="${CHANGES}• $(strip_prefix "$msg")"$'\n' ;; esac done < <(git log --pretty=format:%s "${PREV_TAG}..${TAG}") if [ -z "$CHANGES" ]; then CHANGES="• See release notes."$'\n' fi # HTML-escape the changelog before injecting into Telegram HTML # mode — commit messages can legitimately contain `<`, `>`, `&`. ESCAPED_CHANGES=$(printf '%s' "$CHANGES" \ | 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"))"