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
62 changes: 58 additions & 4 deletions .github/blocks/determine-publish-version/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ outputs:
previous-version:
description: 'The previous version before this release'
value: ${{ steps.last-release.outputs.version }}
release-type:
description: 'The bump type relative to the last STABLE (non-prerelease) release: major | minor | patch. Comparing against the last stable, not the last release-of-any-kind, so GA finalization after a prerelease (2.0.0-rc.3 -> 2.0.0) is correctly classified as the original bump (here, major) instead of patch.'
value: ${{ steps.classify.outputs.kind }}

runs:
using: 'composite'
Expand All @@ -27,14 +30,23 @@ runs:
with:
script: |
try {
// Pull a page of recent releases so we can pick out both the
// last release of any kind (used for the AI commit range —
// the AI should only describe NEW changes since the previous
// tag, prerelease or not) and the last STABLE release (used
// for semver classification — a GA finalization of an -rc
// should classify as the original bump kind, not patch).
const { data: releases } = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 1
per_page: 100
});

if (releases.length > 0) {
const tagName = releases[0].tag_name;
const nonDraft = releases.filter(r => !r.draft);
const latest = nonDraft[0];
const lastStable = nonDraft.find(r => !r.prerelease);

if (latest) {
const tagName = latest.tag_name;
const { data: ref } = await github.rest.git.getRef({
owner: context.repo.owner,
repo: context.repo.repo,
Expand All @@ -51,9 +63,17 @@ runs:
core.setOutput('sha', allCommits[allCommits.length - 1].sha);
core.setOutput('version', '0.0.0');
}

// For classification: prefer the last stable. Falls back to
// `0.0.0` if no stable release exists yet (initial-release path).
core.setOutput(
'stable_version',
lastStable ? lastStable.tag_name.replace(/^v/, '') : '0.0.0'
);
} catch (error) {
core.setOutput('sha', context.sha);
core.setOutput('version', '0.0.0');
core.setOutput('stable_version', '0.0.0');
}

- name: AI Determine Version
Expand Down Expand Up @@ -102,3 +122,37 @@ runs:
fi

echo "Determined version: ${NEXT}"

- name: Classify Bump
id: classify
shell: bash
env:
# Use the last STABLE version (not last-of-any-kind) so that
# finalizing a prerelease into GA (e.g. 2.0.0-rc.3 -> 2.0.0)
# classifies as the original bump (major here), not patch.
# Without this, prev_core == next_core after stripping suffixes
# and the GA's notify path silently downgrades to patch.
PREV: ${{ steps.last-release.outputs.stable_version }}
NEXT: ${{ steps.version.outputs.final }}
run: |
# Strip any pre-release suffix (e.g. -rc.5) on NEXT before
# comparing. PREV is already stable so it has no suffix.
prev_core="${PREV%%-*}"
next_core="${NEXT%%-*}"

IFS='.' read -r p_major p_minor p_patch <<< "$prev_core"
IFS='.' read -r n_major n_minor n_patch <<< "$next_core"

: "${p_major:=0}"; : "${p_minor:=0}"; : "${p_patch:=0}"
: "${n_major:=0}"; : "${n_minor:=0}"; : "${n_patch:=0}"

if [ "$n_major" -gt "$p_major" ]; then
kind="major"
elif [ "$n_minor" -gt "$p_minor" ]; then
kind="minor"
else
kind="patch"
fi

echo "kind=$kind" >> "$GITHUB_OUTPUT"
echo "Classified bump: $prev_core -> $next_core => $kind"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
3 changes: 3 additions & 0 deletions .github/blocks/generate-release-info/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ outputs:
release_notes:
description: 'AI-generated release notes in markdown'
value: ${{ steps.ai-notes.outputs.final-message }}
release_type:
description: 'The bump type relative to previous-version: major | minor | patch'
value: ${{ steps.version-info.outputs.release-type }}

runs:
using: 'composite'
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/typescript-service-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ on:
required: false
default: false
description: "Opt in to npm OIDC Trusted Publishing. When true, the publish job inherits the caller's GITHUB_TOKEN so it can use 'id-token: write' (the caller must grant it) and a trusted publisher must be configured on npmjs.com. Falls back to NPM_TOKEN if OIDC is unavailable. When false (default), publishing uses NPM_TOKEN and the job keeps least-privilege 'contents: read'."
notify-on-release:
type: boolean
required: false
default: true
description: "Fire the post-release notify job on real releases (major | minor | patch). Callers not on the in-job allow-list skip cleanly. Set to false to opt a specific release out."
secrets:
OPENAI_API_KEY:
required: true
Expand All @@ -80,6 +85,12 @@ on:
APP_PRIVATE_KEY:
required: false
description: "GitHub App private key for pushing to protected branches and triggering downstream workflows"
TS_OAUTH_CLIENT_ID:
required: false
description: "Tailscale OAuth client ID — used by the notify job to join the tailnet ephemerally"
TS_OAUTH_SECRET:
required: false
description: "Tailscale OAuth client secret matching TS_OAUTH_CLIENT_ID"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -109,6 +120,7 @@ jobs:
outputs:
version: ${{ steps.generate.outputs.version }}
release_notes: ${{ steps.generate.outputs.release_notes }}
release_type: ${{ steps.generate.outputs.release_type }}
steps:
- uses: actions/checkout@v5
with:
Expand Down Expand Up @@ -209,3 +221,64 @@ jobs:
publish-command: ${{ inputs.publish-command }}
dry-run: ${{ inputs.dry-run }}
npm-token: ${{ secrets.NPM_TOKEN }}

notify:
needs: [check-labels, release-info, github-release, npm-publish, npm-publish-oidc]
if: >-
always() &&
github.repository == 'photon-hq/spectrum-ts' &&
(fromJSON(needs.check-labels.outputs.labels).release || inputs.release) &&
needs.release-info.result == 'success' &&
needs.github-release.result == 'success' &&
(needs.npm-publish.result == 'success' || needs.npm-publish.result == 'skipped') &&
(needs.npm-publish-oidc.result == 'success' || needs.npm-publish-oidc.result == 'skipped') &&
inputs.notify-on-release &&
contains(fromJSON('["major","minor","patch"]'), needs.release-info.outputs.release_type)
Comment thread
yanxue06 marked this conversation as resolved.
runs-on: ${{ inputs.use-blacksmith && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
permissions:
contents: read
steps:
- name: Join Tailnet
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:infra
version: latest
Comment thread
yanxue06 marked this conversation as resolved.

- name: POST to email-monkey
env:
SERVICE_NAME: ${{ inputs.service-name }}
REPO: ${{ github.repository }}
VERSION: ${{ needs.release-info.outputs.version }}
RELEASE_NOTES: ${{ needs.release-info.outputs.release_notes }}
RELEASE_TYPE: ${{ needs.release-info.outputs.release_type }}
shell: bash
run: |
set -euo pipefail
BODY="$(jq -nc \
--arg repo "$REPO" \
--arg version "$VERSION" \
--arg releaseNotes "$RELEASE_NOTES" \
--arg releaseType "$RELEASE_TYPE" \
--arg serviceName "$SERVICE_NAME" \
'{repo:$repo, version:$version, releaseNotes:$releaseNotes, releaseType:$releaseType, serviceName:$serviceName}')"

echo "POSTing to email-monkey for $REPO (release_type=$RELEASE_TYPE, version=$VERSION)"

HTTP_CODE="$(curl --silent --show-error \
--output /tmp/email-monkey.out \
--write-out '%{http_code}' \
--max-time 60 \
--request POST "http://email-monkey/notify-release" \
--header "Content-Type: application/json" \
--data "$BODY")"

echo "email-monkey response ($HTTP_CODE):"
cat /tmp/email-monkey.out || true
echo

if [ "$HTTP_CODE" -ge 400 ]; then
echo "::error::email-monkey returned HTTP $HTTP_CODE"
exit 1
fi
Loading