Skip to content

Draft a new release #199

Draft a new release

Draft a new release #199

name: Draft a new release
on:
workflow_dispatch:
inputs:
release_ticket_id:
description: Release ticket ID (Ex:- SDK-1234)
required: true
slack_message_link:
description: Slack message URL of the release
required: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_OPTIONS: '--no-warnings'
permissions:
contents: read
jobs:
validate-actor:
permissions:
contents: read
# Only allow to draft a new release from develop and hotfix branches
if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/hotfix/')
uses: ./.github/workflows/validate-actor.yml
with:
team_names: 'js-sdk,integrations'
secrets:
RELEASE_PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }}
draft-new-release:
needs: validate-actor
name: Draft a new release
runs-on: [self-hosted, Linux, X64]
permissions:
contents: read # to checkout repository code
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ vars.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_PRIVATE_KEY }}
permission-contents: write # to create commits, tags, and push branches
permission-pull-requests: write # to create and update PRs
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ steps.generate-token.outputs.token }}
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Setup workspace
env:
HUSKY: 0
run: |
npm run setup:deps
# In order to make a commit, we need to initialize a user.
# You may choose to write something less generic here if you want, it doesn't matter functionality wise.
- name: Initialize mandatory git config
run: |
git config user.name "GitHub actions"
git config user.email noreply@github.com
# Calculate the next release version based on conventional semantic release
- name: Create release branch
id: create-release
env:
HUSKY: 0
run: |
source_branch_name=${GITHUB_REF#refs/heads/}
release_type=release
grep -q "hotfix/" <<< "${GITHUB_REF}" && release_type=hotfix-release
git fetch origin main
git fetch --tags origin
git merge origin/main
current_version=$(jq -r .version package.json)
npm run bump-version:monorepo
new_version=$(jq -r .version package.json)
git reset --hard
branch_name="${release_type}/${new_version}-${{ github.event.inputs.release_ticket_id }}"
echo "Source branch for new release is $source_branch_name"
echo "Current version is $current_version"
echo "Release type is $release_type"
echo "New version is $new_version"
echo "New release branch name is $branch_name"
git checkout -b "$branch_name"
echo "source_branch_name=$source_branch_name" >> $GITHUB_OUTPUT
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
echo "new_version=$new_version" >> $GITHUB_OUTPUT
echo "CURRENT_VERSION_VALUE=$current_version" >> $GITHUB_ENV
echo "NEW_VERSION_VALUE=$new_version" >> $GITHUB_ENV
- name: Update changelog & bump version
id: finish-release
env:
HUSKY: 0
BASE_BRANCH: ${{ steps.create-release.outputs.branch_name }}
run: |
echo "Current version: $CURRENT_VERSION_VALUE"
echo "New version: $NEW_VERSION_VALUE"
git fetch --no-tags --prune --depth=100 origin main
npm run release
./scripts/sync-tags-in-nx-projects.sh
./scripts/generate-last-release-changelog.sh
npm run bump-version:monorepo
npx replace $CURRENT_VERSION_VALUE $NEW_VERSION_VALUE sonar-project.properties
npm install
npm run clean
- name: Generate packages versions table
id: packages-versions
run: |
chmod +x ./scripts/collect-packages-versions.sh
./scripts/collect-packages-versions.sh > packages_versions.md
- name: Create release branch on remote
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
BRANCH_NAME: ${{ steps.create-release.outputs.branch_name }}
REPO: ${{ github.repository }}
run: |
# The signed-commit action requires the branch to exist on the remote.
# Idempotent: update ref if it exists, create if it doesn't.
sha=$(git rev-parse HEAD)
if gh api "repos/${REPO}/git/ref/heads/${BRANCH_NAME}" >/dev/null 2>&1; then
gh api "repos/${REPO}/git/refs/heads/${BRANCH_NAME}" \
--method PATCH \
-f "sha=${sha}" \
-F "force=true"
else
gh api "repos/${REPO}/git/refs" \
--method POST \
-f "ref=refs/heads/${BRANCH_NAME}" \
-f "sha=${sha}"
fi
- name: Create verified commit and push release branch
uses: ryancyq/github-signed-commit@e9f3b28c80da7be66d24b8f501a5abe82a6b855f # v1.2.0
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
with:
branch-name: ${{ steps.create-release.outputs.branch_name }}
commit-message: 'chore(monorepo): sync versions and generate release logs'
files: |
.
- name: Create pull request into main
id: create-pr
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
pr_title="chore(release): merge ${{ steps.create-release.outputs.branch_name }} into main [${{ github.event.inputs.release_ticket_id }}]"
# Create PR body with proper formatting
cat > pr_body.md << EOF
:crown: **Automated Release PR**
This pull request was created automatically by the GitHub Actions workflow. It merges the release branch (\`${{ steps.create-release.outputs.branch_name }}\`) into the \`main\` branch.
This ensures that the latest release branch changes are incorporated into the \`main\` branch for production.
### Details
- **Monorepo Release Version**: v${{ env.NEW_VERSION_VALUE }}
- **Release Branch**: \`${{ steps.create-release.outputs.branch_name }}\`
- **Related Ticket**: [${{ github.event.inputs.release_ticket_id }}](https://linear.app/rudderstack/issue/${{ github.event.inputs.release_ticket_id }})
### Version updates
$(cat packages_versions.md)
---
Please review and merge when ready. :rocket:
EOF
# Create the PR using GitHub CLI and capture the URL
pr_url=$(gh pr create \
--base main \
--head ${{ steps.create-release.outputs.branch_name }} \
--title "$pr_title" \
--body-file pr_body.md)
echo "pr_url=$pr_url" >> $GITHUB_OUTPUT
- name: Extract Slack channel and thread info
id: slack-info
continue-on-error: true
run: |
link="${{ github.event.inputs.slack_message_link }}"
# Extract channel ID from the link (after /archives/ and before the next /)
channel_id=$(echo "$link" | sed -n 's|.*\/archives\/\([^\/]*\)\/.*|\1|p')
# Check if thread_ts parameter exists in URL (for messages within a thread)
if echo "$link" | grep -q "thread_ts="; then
# Extract thread_ts parameter value
thread_ts=$(echo "$link" | sed -n 's|.*thread_ts=\([0-9]*\.[0-9]*\).*|\1|p')
echo "Using thread_ts from URL parameter: $thread_ts"
else
# Extract timestamp from /p part and convert to timestamp format (for parent messages)
thread_ts=$(echo "$link" | sed -n 's|.*\/p\([0-9]*\).*|\1|p')
# Convert from Slack link format (p1640995200123456) to timestamp format (1640995200.123456)
if [ ${#thread_ts} -eq 16 ]; then
# Split into seconds and microseconds parts
seconds=${thread_ts:0:10}
microseconds=${thread_ts:10:6}
thread_ts="${seconds}.${microseconds}"
fi
echo "Using timestamp from /p part: $thread_ts"
fi
echo "Extracted channel ID: $channel_id"
echo "Final thread timestamp: $thread_ts"
echo "channel_id=$channel_id" >> $GITHUB_OUTPUT
echo "thread_ts=$thread_ts" >> $GITHUB_OUTPUT
- name: Create release info artifact
if: ${{ steps.slack-info.outputs.thread_ts != '' && steps.slack-info.outputs.channel_id != '' }}
run: |
# Create directory for artifact
mkdir -p release-info
# Create JSON file with release information
cat > release-info/release-info.json << EOF
{
"channel_id": "${{ steps.slack-info.outputs.channel_id }}",
"thread_ts": "${{ steps.slack-info.outputs.thread_ts }}",
"pr_url": "${{ steps.create-pr.outputs.pr_url }}",
"release_version": "${{ env.NEW_VERSION_VALUE }}",
"release_branch": "${{ steps.create-release.outputs.branch_name }}",
"release_ticket_id": "${{ github.event.inputs.release_ticket_id }}",
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
echo "Created release info artifact:"
cat release-info/release-info.json
- name: Upload release info artifact
if: ${{ steps.slack-info.outputs.thread_ts != '' && steps.slack-info.outputs.channel_id != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: release-info-v${{ env.NEW_VERSION_VALUE }}-${{ github.event.inputs.release_ticket_id }}
path: release-info/
retention-days: 30
- name: Reply in Slack thread
if: ${{ steps.slack-info.outputs.thread_ts != '' && steps.slack-info.outputs.channel_id != '' }}
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
continue-on-error: true
with:
method: chat.postMessage
payload-templated: true
retries: rapid
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
{
"channel": "${{ steps.slack-info.outputs.channel_id }}",
"thread_ts": "${{ steps.slack-info.outputs.thread_ts }}",
"text": ":rocket: Release PR created: <${{ steps.create-pr.outputs.pr_url }}|${{ steps.create-pr.outputs.pr_url }}>\nCC: <!subteam^S0555JBV36D> <!subteam^S03SW7DM8P3> <!subteam^S03SHJ20350>",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rocket: *Release PR Created*: \n<${{ steps.create-pr.outputs.pr_url }}|${{ steps.create-pr.outputs.pr_url }}>\n\n*Monorepo Version:* v${{ env.NEW_VERSION_VALUE }}\n*Release Branch:* `${{ steps.create-release.outputs.branch_name }}`\n\n:point_right: Review and merge when ready!\nCC: <!subteam^S0555JBV36D> <!subteam^S03SW7DM8P3> <!subteam^S03SHJ20350>"
}
}
]
}
- name: Delete hotfix release base branch
continue-on-error: true
if: startsWith(github.ref_name, 'hotfix/')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const branchToDelete = '${{ github.ref_name }}';
const owner = context.repo.owner;
const repo = context.repo.repo;
const ref = `heads/${branchToDelete}`;
try {
await github.rest.git.deleteRef({
owner,
repo,
ref
});
console.log(`Branch ${branchToDelete} deleted successfully.`);
} catch (error) {
console.error(`Error deleting branch ${branchToDelete}:`, error);
process.exit(1); // Fail the workflow if branch deletion fails
}