Skip to content

Commit e585d60

Browse files
yanxue06cursoragent
andcommitted
feat(typescript-service-release): add notify-on-release email pipeline
Brings the email-monkey notify job into the public buildspace so PUBLIC caller repos (e.g. photon-hq/spectrum-ts) can get release-notification emails through plain `uses:` without needing the public->internal App-token + REST-dispatch workaround. Three coupled additions, atomic because notify needs release_type: 1. .github/blocks/determine-publish-version/action.yaml - new `release-type` output classifying the bump (major|minor|patch) against `previous-version`; strips any -rc.N suffix before comparing. 2. .github/blocks/generate-release-info/action.yaml - forwards `release_type` from the underlying version block. 3. .github/workflows/typescript-service-release.yaml - new `notify-on-release` input (default true). - new optional `TS_OAUTH_CLIENT_ID` / `TS_OAUTH_SECRET` secrets (Tailscale OAuth for ephemeral tailnet join as tag:ci). - `release-info` job exposes `release_type` for downstream jobs. - new `notify` job: resolves per-repo email-monkey URL (map of caller -> tailnet host; unknown callers exit cleanly), joins tailnet, POSTs {repo, version, releaseNotes, releaseType, serviceName} to email-monkey. Fires only on real major/minor releases. Patch releases short-circuit; unknown callers short-circuit; missing TS_OAUTH_* secrets fail loudly on the tailnet join step (intentional, since you can't reach email-monkey without them). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 55b2b4b commit e585d60

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

.github/blocks/determine-publish-version/action.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ outputs:
1717
previous-version:
1818
description: 'The previous version before this release'
1919
value: ${{ steps.last-release.outputs.version }}
20+
release-type:
21+
description: 'The bump type relative to previous-version: major | minor | patch'
22+
value: ${{ steps.classify.outputs.kind }}
2023

2124
runs:
2225
using: 'composite'
@@ -102,3 +105,31 @@ runs:
102105
fi
103106
104107
echo "Determined version: ${NEXT}"
108+
109+
- name: Classify Bump
110+
id: classify
111+
shell: bash
112+
env:
113+
PREV: ${{ steps.last-release.outputs.version }}
114+
NEXT: ${{ steps.version.outputs.final }}
115+
run: |
116+
# Strip any pre-release suffix (e.g. -rc.5) before comparing.
117+
prev_core="${PREV%%-*}"
118+
next_core="${NEXT%%-*}"
119+
120+
IFS='.' read -r p_major p_minor p_patch <<< "$prev_core"
121+
IFS='.' read -r n_major n_minor n_patch <<< "$next_core"
122+
123+
: "${p_major:=0}"; : "${p_minor:=0}"; : "${p_patch:=0}"
124+
: "${n_major:=0}"; : "${n_minor:=0}"; : "${n_patch:=0}"
125+
126+
if [ "$n_major" -gt "$p_major" ]; then
127+
kind="major"
128+
elif [ "$n_minor" -gt "$p_minor" ]; then
129+
kind="minor"
130+
else
131+
kind="patch"
132+
fi
133+
134+
echo "kind=$kind" >> "$GITHUB_OUTPUT"
135+
echo "Classified bump: $prev_core -> $next_core => $kind"

.github/blocks/generate-release-info/action.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ outputs:
2020
release_notes:
2121
description: 'AI-generated release notes in markdown'
2222
value: ${{ steps.ai-notes.outputs.final-message }}
23+
release_type:
24+
description: 'The bump type relative to previous-version: major | minor | patch'
25+
value: ${{ steps.version-info.outputs.release-type }}
2326

2427
runs:
2528
using: 'composite'

.github/workflows/typescript-service-release.yaml

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ on:
6262
required: false
6363
default: false
6464
description: "Opt in to Blacksmith Linux runners (blacksmith-4vcpu-ubuntu-2404). Requires the Blacksmith GitHub App installed on the caller org. Defaults to ubuntu-latest."
65+
notify-on-release:
66+
type: boolean
67+
required: false
68+
default: true
69+
description: "Send a release-notification email via email-monkey on major/minor bumps. The endpoint URL is resolved per calling repo inside the notify job; an unknown caller skips the notify job entirely."
6570
secrets:
6671
OPENAI_API_KEY:
6772
required: true
@@ -75,6 +80,12 @@ on:
7580
APP_PRIVATE_KEY:
7681
required: false
7782
description: "GitHub App private key for pushing to protected branches and triggering downstream workflows"
83+
TS_OAUTH_CLIENT_ID:
84+
required: false
85+
description: "Tailscale OAuth client ID (scope: auth_keys, tag owner of tag:ci) — used by the notify job to join the tailnet ephemerally"
86+
TS_OAUTH_SECRET:
87+
required: false
88+
description: "Tailscale OAuth client secret matching TS_OAUTH_CLIENT_ID"
7889

7990
concurrency:
8091
group: ${{ github.workflow }}-${{ github.ref }}
@@ -104,6 +115,7 @@ jobs:
104115
outputs:
105116
version: ${{ steps.generate.outputs.version }}
106117
release_notes: ${{ steps.generate.outputs.release_notes }}
118+
release_type: ${{ steps.generate.outputs.release_type }}
107119
steps:
108120
- uses: actions/checkout@v5
109121
with:
@@ -175,3 +187,101 @@ jobs:
175187
publish-command: ${{ inputs.publish-command }}
176188
dry-run: ${{ inputs.dry-run }}
177189
npm-token: ${{ secrets.NPM_TOKEN }}
190+
191+
notify:
192+
needs: [check-labels, release-info, github-release, npm-publish]
193+
# Only fire on real releases of feature/breaking bumps. Patch releases never email.
194+
# The notify job itself short-circuits if github.repository isn't in the
195+
# per-repo URL map below, so unknown callers skip cleanly with no error.
196+
if: >-
197+
always() &&
198+
(fromJSON(needs.check-labels.outputs.labels).release || inputs.release) &&
199+
needs.release-info.result == 'success' &&
200+
needs.github-release.result == 'success' &&
201+
(needs.npm-publish.result == 'success' || needs.npm-publish.result == 'skipped') &&
202+
inputs.notify-on-release &&
203+
contains(fromJSON('["major","minor"]'), needs.release-info.outputs.release_type)
204+
runs-on: ${{ inputs.use-blacksmith && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
205+
permissions:
206+
contents: read
207+
steps:
208+
# Per-repo email-monkey URL map. Add new repos here as we onboard them.
209+
# An empty url means "this caller isn't wired up yet" and the job exits 0.
210+
#
211+
# URL format: MagicDNS hostname assigned by the Tailscale K8s operator
212+
# when the Service is created with loadBalancerClass: tailscale. By
213+
# convention we name the Service `email-monkey` so the hostname is
214+
# `email-monkey.<tailnet>.ts.net`. Port 80 on the tailnet is fine —
215+
# Tailscale itself encrypts every hop end-to-end.
216+
- name: Resolve email-monkey URL
217+
id: monkey
218+
env:
219+
REPO: ${{ github.repository }}
220+
shell: bash
221+
run: |
222+
set -euo pipefail
223+
case "$REPO" in
224+
photon-hq/spectrum-ts)
225+
echo "url=http://email-monkey/notify-release" >> "$GITHUB_OUTPUT"
226+
;;
227+
*)
228+
echo "::notice::No email-monkey URL configured for $REPO; skipping notify."
229+
echo "url=" >> "$GITHUB_OUTPUT"
230+
;;
231+
esac
232+
233+
- name: Join Tailnet
234+
if: steps.monkey.outputs.url != ''
235+
uses: tailscale/github-action@v3
236+
with:
237+
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
238+
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
239+
tags: tag:ci
240+
version: latest
241+
242+
- name: POST to email-monkey
243+
if: steps.monkey.outputs.url != ''
244+
env:
245+
EMAIL_MONKEY_URL: ${{ steps.monkey.outputs.url }}
246+
SERVICE_NAME: ${{ inputs.service-name }}
247+
REPO: ${{ github.repository }}
248+
VERSION: ${{ needs.release-info.outputs.version }}
249+
RELEASE_NOTES: ${{ needs.release-info.outputs.release_notes }}
250+
RELEASE_TYPE: ${{ needs.release-info.outputs.release_type }}
251+
shell: bash
252+
run: |
253+
set -euo pipefail
254+
255+
# Auth: TAILSCALE-ONLY. email-monkey is only reachable from this
256+
# runner because the Tailscale ACL allows `tag:ci` to reach it.
257+
# No application-layer auth headers needed.
258+
259+
# NOTE: do not send `tag` here. email-monkey derives the release tag
260+
# and marker tag from `version` alone to guarantee one canonical
261+
# format across every caller. Adding fields here will not change that.
262+
BODY="$(jq -nc \
263+
--arg repo "$REPO" \
264+
--arg version "$VERSION" \
265+
--arg releaseNotes "$RELEASE_NOTES" \
266+
--arg releaseType "$RELEASE_TYPE" \
267+
--arg serviceName "$SERVICE_NAME" \
268+
'{repo:$repo, version:$version, releaseNotes:$releaseNotes, releaseType:$releaseType, serviceName:$serviceName}')"
269+
270+
echo "POSTing to $EMAIL_MONKEY_URL (release_type=$RELEASE_TYPE, version=$VERSION)"
271+
272+
HTTP_CODE="$(curl --silent --show-error \
273+
--output /tmp/email-monkey.out \
274+
--write-out '%{http_code}' \
275+
--max-time 60 \
276+
--request POST "$EMAIL_MONKEY_URL" \
277+
--header "Content-Type: application/json" \
278+
--data "$BODY")"
279+
280+
echo "email-monkey response ($HTTP_CODE):"
281+
cat /tmp/email-monkey.out || true
282+
echo
283+
284+
if [ "$HTTP_CODE" -ge 400 ]; then
285+
echo "::error::email-monkey returned HTTP $HTTP_CODE"
286+
exit 1
287+
fi

0 commit comments

Comments
 (0)