11name : sdk-release-finalize
22
33# Reusable workflow: create release-discovery tags after registry publishing
4- # succeeds. Call this from SDK repos after npm/PyPI/Maven publish completes,
4+ # succeeds and preserve Fern's current replay generation commit for the next
5+ # autorelease. Call this from SDK repos after npm/PyPI/Maven publish completes,
56# passing the version_tag, origin_commit, and origin_tag outputs from
67# sdk-release-gate. Keeping these side effects post-publish means failed
78# registry publishes can be retried without a pre-created tag blocking them.
2425 description : " Git revision to tag. Defaults to the caller workflow commit."
2526 type : string
2627 default : " HEAD"
28+ replay-lock-path :
29+ description : " Path to Fern's replay.lock within the caller repo."
30+ type : string
31+ default : " .fern/replay.lock"
2732 create-github-release :
2833 description : " Whether to create a GitHub release for the version tag."
2934 type : boolean
4146 origin_tag_created :
4247 description : " true when a new origin release tag was pushed; false when it already existed."
4348 value : ${{ jobs.finalize.outputs.origin_tag_created }}
49+ fern_generation_tag :
50+ description : " The Fern replay generation tag created or verified by this workflow, or empty when replay.lock is absent."
51+ value : ${{ jobs.finalize.outputs.fern_generation_tag }}
52+ fern_generation_tag_created :
53+ description : " true when a new Fern replay generation tag was pushed; false when it already existed or replay.lock is absent."
54+ value : ${{ jobs.finalize.outputs.fern_generation_tag_created }}
4455
4556permissions :
4657 contents : write
@@ -53,11 +64,14 @@ jobs:
5364 version_tag_created : ${{ steps.version-tag-output.outputs.created }}
5465 origin_tag : ${{ steps.inputs.outputs.origin_tag }}
5566 origin_tag_created : ${{ steps.origin-tag.outputs.created }}
67+ fern_generation_tag : ${{ steps.fern-generation-tag.outputs.tag }}
68+ fern_generation_tag_created : ${{ steps.fern-generation-tag.outputs.created }}
5669 env :
5770 VERSION_TAG : ${{ inputs.version-tag }}
5871 ORIGIN_COMMIT : ${{ inputs.origin-commit }}
5972 ORIGIN_TAG : ${{ inputs.origin-tag }}
6073 TAG_TARGET : ${{ inputs.tag-target }}
74+ REPLAY_LOCK_PATH : ${{ inputs.replay-lock-path }}
6175 steps :
6276 - name : Checkout release commit
6377 uses : actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
@@ -198,3 +212,67 @@ jobs:
198212 git push origin "refs/tags/$ORIGIN_TAG"
199213
200214 echo "created=true" >> "$GITHUB_OUTPUT"
215+
216+ - name : Preserve Fern replay generation
217+ id : fern-generation-tag
218+ shell : bash
219+ run : |
220+ set -euo pipefail
221+
222+ release_commit=$(git rev-parse --verify "${TAG_TARGET}^{commit}")
223+ if ! replay_lock=$(git show "${release_commit}:${REPLAY_LOCK_PATH}" 2>/dev/null); then
224+ echo "::notice::$REPLAY_LOCK_PATH does not exist at $release_commit; skipping Fern replay generation tag."
225+ {
226+ echo "tag="
227+ echo "created=false"
228+ } >> "$GITHUB_OUTPUT"
229+ exit 0
230+ fi
231+
232+ generation_commit=$(printf '%s\n' "$replay_lock" | sed -n 's/^current_generation:[[:space:]]*//p' | head -n1)
233+ if [[ -z "$generation_commit" ]]; then
234+ echo "::error::$REPLAY_LOCK_PATH at $release_commit has no current_generation field."
235+ exit 1
236+ fi
237+ if ! printf '%s' "$generation_commit" | grep -Eq '^[0-9a-f]{40}$'; then
238+ echo "::error::Invalid Fern current_generation SHA: $generation_commit"
239+ exit 1
240+ fi
241+
242+ generation_tag="fern-generation/$generation_commit"
243+ if ! git check-ref-format "refs/tags/$generation_tag"; then
244+ echo "::error::Invalid Fern generation tag name: $generation_tag"
245+ exit 1
246+ fi
247+
248+ if ! target_commit=$(git rev-parse --verify "${generation_commit}^{commit}" 2>/dev/null); then
249+ if ! git fetch --no-tags --depth=1 origin "$generation_commit"; then
250+ echo "::error::Fern current_generation commit $generation_commit is not present in the checkout and could not be fetched from origin. Ensure it is reachable from a remote ref before finalizing."
251+ exit 1
252+ fi
253+ target_commit=$(git rev-parse --verify "${generation_commit}^{commit}")
254+ fi
255+
256+ existing_commit=$(git rev-parse -q --verify "refs/tags/${generation_tag}^{commit}" || true)
257+
258+ if [[ -n "$existing_commit" ]]; then
259+ if [[ "$existing_commit" == "$target_commit" ]]; then
260+ echo "Tag $generation_tag already points at $target_commit."
261+ {
262+ echo "tag=$generation_tag"
263+ echo "created=false"
264+ } >> "$GITHUB_OUTPUT"
265+ exit 0
266+ fi
267+
268+ echo "::error::Tag $generation_tag already points at $existing_commit, not $target_commit."
269+ exit 1
270+ fi
271+
272+ git tag "$generation_tag" "$target_commit"
273+ git push origin "refs/tags/$generation_tag"
274+
275+ {
276+ echo "tag=$generation_tag"
277+ echo "created=true"
278+ } >> "$GITHUB_OUTPUT"
0 commit comments