Skip to content
Closed
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
76 changes: 56 additions & 20 deletions .github/workflows/prepare-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
# with the required `package` input; the workflow then, for THAT package only:
# 1. Bumps the selected package's version in its own package.json
# 2. Generates a changelog from git history since the package's last release tag
# 3. Updates the package's changelog file and creates a release branch + package-scoped tag
# 3. Updates the package's changelog file and creates a release branch
# Packages are versioned and tagged independently (tag prefixes: v, azuremanaged-v, durable-functions-v).
# After the workflow completes, manually open a PR from the release branch to main, then publish
# from the package directory using the npm command shown in the run summary (mind the dist-tag).
# After the workflow completes, manually open and merge a PR from the release branch to main.
# Only then create the package-scoped tag on the exact merged commit and publish.

name: Prepare Release

Expand Down Expand Up @@ -122,24 +122,44 @@ jobs:
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version: $NEW_VERSION"

- name: Verify target release tag is unused
id: target-tag
env:
NEW_VERSION: ${{ steps.calc-version.outputs.new_version }}
TAG_PREFIX: ${{ steps.resolve-package.outputs.tag_prefix }}
run: |
TAG_NAME="${TAG_PREFIX}${NEW_VERSION}"
Comment thread
YunchuWang marked this conversation as resolved.
if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" >/dev/null; then
echo "::error::Release tag ${TAG_NAME} already exists on origin. Refusing to prepare another release for an immutable tag."
exit 1
else
STATUS=$?
if [ "$STATUS" -ne 2 ]; then
echo "::error::Could not verify whether release tag ${TAG_NAME} exists on origin."
exit "$STATUS"
fi
fi
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "Verified release tag ${TAG_NAME} does not exist on origin"

- name: Get latest release tag
id: get-latest-tag
env:
PACKAGE: ${{ github.event.inputs.package }}
CURRENT_VERSION: ${{ steps.get-current-version.outputs.current_version }}
NEW_VERSION: ${{ steps.calc-version.outputs.new_version }}
TAG_PREFIX: ${{ steps.resolve-package.outputs.tag_prefix }}
TARGET_TAG: ${{ steps.target-tag.outputs.tag_name }}
run: |
# Get the latest tag for THIS package (its tag prefix), excluding the tag being
# created so that re-runs don't use it as the baseline. The legacy core prefix "v"
# Get the latest tag for THIS package (its tag prefix), excluding the target release
# tag as a defense against it appearing after the validation step. The legacy core prefix "v"
# still matches historical tags like v0.3.0 (intended, backward compatible) and never
# matches the "azuremanaged-v"/"durable-functions-v" namespaces (they start with a/d, not v).
# Without versionsort.suffix=-, git ranks a prerelease (e.g. X-beta.1) ABOVE its own GA
# (X), so the release AFTER GA (e.g. X.0.1) would resolve LATEST_TAG to the prerelease and
# re-list every commit already shipped in GA. Treating "-" as the prerelease separator
# orders X-beta.1 below X.
LATEST_TAG=$(git -c versionsort.suffix=- tag -l "${TAG_PREFIX}*" --sort=-v:refname | \
grep -v "^${TAG_PREFIX}${NEW_VERSION}$" | head -n 1)
grep -Fvx -- "$TARGET_TAG" | head -n 1)
if [ -z "$LATEST_TAG" ]; then
LEGACY_TAG="v${CURRENT_VERSION}"
if [ "$PACKAGE" = "durabletask-js-azuremanaged" ] && \
Expand Down Expand Up @@ -253,33 +273,29 @@ jobs:
NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"
PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}"
NPM_NAME="${{ steps.resolve-package.outputs.npm_name }}"
TAG_PREFIX="${{ steps.resolve-package.outputs.tag_prefix }}"
CHANGELOG_PATH="${{ steps.resolve-package.outputs.changelog_path }}"
TAG_NAME="${TAG_PREFIX}${NEW_VERSION}"
TAG_NAME="${{ steps.target-tag.outputs.tag_name }}"
BRANCH_NAME="release/${TAG_NAME}"

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# Create and checkout release branch (idempotent)
git checkout -B "$BRANCH_NAME"
# Create and checkout a new release branch.
git checkout -b "$BRANCH_NAME"

# Stage and commit changes (only the released package, lockfile, and its changelog)
git add "${PKG_DIR}/package.json"
git add package-lock.json
git add "${CHANGELOG_PATH}"
git commit -m "Release ${NPM_NAME}@${NEW_VERSION}"

# Create release tag (idempotent)
git tag -f "$TAG_NAME"

# Push branch and tag (force to handle re-runs)
git push -f origin "$BRANCH_NAME"
git push -f origin "$TAG_NAME"
# Never overwrite an existing release branch. A conflicting re-run must fail so its
# version and changelog changes can be reviewed instead of replacing remote history.
git push origin "$BRANCH_NAME"

echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "Created branch $BRANCH_NAME and tag $TAG_NAME"
echo "Created branch $BRANCH_NAME; create tag $TAG_NAME only after the PR merges"

- name: Summary
run: |
Expand All @@ -302,17 +318,37 @@ jobs:
echo "- **Package**: ${NPM_NAME}" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: ${NEW_VERSION}" >> $GITHUB_STEP_SUMMARY
echo "- **Branch**: ${BRANCH_NAME}" >> $GITHUB_STEP_SUMMARY
echo "- **Tag**: ${TAG_NAME}" >> $GITHUB_STEP_SUMMARY
echo "- **Post-merge tag**: ${TAG_NAME} (not created by this workflow)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Next Step: Create a Pull Request" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Manually create a PR from **${BRANCH_NAME}** → **main** with title: **Release ${NPM_NAME}@${NEW_VERSION}**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "[Create PR](https://github.com/microsoft/durabletask-js/compare/main...${BRANCH_NAME}?expand=1&title=Release+${NPM_NAME}@${NEW_VERSION})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### After Merge: Tag the Exact Merged Commit" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Replace \`1234\` with the merged release PR number. These commands deliberately omit \`-f\`: if **${TAG_NAME}** already exists, stop and investigate; never move an existing release tag." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "set -euo pipefail" >> $GITHUB_STEP_SUMMARY
echo "git switch main" >> $GITHUB_STEP_SUMMARY
echo "git pull --ff-only origin main" >> $GITHUB_STEP_SUMMARY
echo "git fetch origin --tags" >> $GITHUB_STEP_SUMMARY
echo "PR_NUMBER=1234 # replace with the merged release PR number" >> $GITHUB_STEP_SUMMARY
echo "MERGED_COMMIT=\$(gh pr view \"\$PR_NUMBER\" --repo microsoft/durabletask-js --json mergeCommit --jq '.mergeCommit.oid')" >> $GITHUB_STEP_SUMMARY
echo "git merge-base --is-ancestor \"\$MERGED_COMMIT\" origin/main" >> $GITHUB_STEP_SUMMARY
echo "test \"\$(git show \"\${MERGED_COMMIT}:${PKG_DIR}/package.json\" | node -p \"JSON.parse(require('fs').readFileSync(0, 'utf8')).version\")\" = \"${NEW_VERSION}\"" >> $GITHUB_STEP_SUMMARY
echo "if git rev-parse --verify --quiet \"refs/tags/${TAG_NAME}\" >/dev/null; then echo \"Tag ${TAG_NAME} already exists; refusing to move it.\" >&2; exit 1; fi" >> $GITHUB_STEP_SUMMARY
echo "git tag \"${TAG_NAME}\" \"\$MERGED_COMMIT\"" >> $GITHUB_STEP_SUMMARY
echo "git push origin \"refs/tags/${TAG_NAME}\"" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Pushing **${TAG_NAME}** triggers the code mirror's dedicated tag path. Before continuing, verify that run mirrored **${TAG_NAME}** to exactly \`\$MERGED_COMMIT\` and re-locked the internal tag. Then use the official \`main\` build whose source commit is exactly \`\$MERGED_COMMIT\`; never substitute a later \`main\` build." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Then Publish" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "After the release PR is merged, publish from the package directory:" >> $GITHUB_STEP_SUMMARY
echo "Use the ADO official build + ESRP release pipeline. If a maintainer must use the documented manual fallback, publish from the package directory:" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "cd ${PKG_DIR}" >> $GITHUB_STEP_SUMMARY
echo "${PUBLISH_CMD}" >> $GITHUB_STEP_SUMMARY
Expand Down
Loading
Loading