Skip to content
Open
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
213 changes: 148 additions & 65 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
name: Auto Release on Release Branch
name: Auto Release on Main Branch

on:
pull_request:
types: [ closed ]
branches: [ release ]
branches: [ main ]

permissions:
contents: write

jobs:
auto-release:
# Only run if PR was merged to release branch (not just closed)
if: github.event.pull_request.merged == true
# Only run if PR was merged from release branch to main and contains release label
if: github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'release' && contains(github.event.pull_request.labels.*.name, 'release')
runs-on: ubuntu-latest

steps:
Expand All @@ -20,96 +20,179 @@ jobs:
with:
fetch-depth: 0

- name: Extract tags from PR labels
id: get_tags_labels
- name: Extract and validate required labels
id: get_labels
run: |
# Get PR labels and extract version
# Get PR labels
LABELS='${{ toJson(github.event.pull_request.labels.*.name) }}'
echo "PR Labels: $LABELS"

# Look for version label (e.g., "v1.0.0", "version:1.0.0", etc.)
VERSION=$(echo $LABELS | jq -r '.[] | select(test("^(v|version:)?[0-9]+\\.[0-9]+\\.[0-9]+")) | gsub("^(v|version:)"; "")')
# Look for version label in x.x.x format (e.g., "1.0.0", "2.1.3") - MANDATORY
VERSION=$(echo $LABELS | jq -r '.[] | select(test("^[0-9]+\\.[0-9]+\\.[0-9]+$"))')

# Look for zeam network tags (devnet0, devnet1, testnet, mainnet)
ZEAM_TAG=$(echo $LABELS | jq -r '.[] | select(test("^(devnet[0-9]+|testnet[0-9]*|mainnet)$"))')
# Look for devnet labels (devnet0, devnet1, devnet2, etc.) - MANDATORY
DEVNET_LABEL=$(echo $LABELS | jq -r '.[] | select(test("^devnet[0-9]+$"))')
Comment on lines +31 to +34
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow extracts label data using jq on JSON that includes user-controlled PR labels. While the current regex patterns provide some validation, if a malicious user creates a label that matches the version or devnet patterns but contains additional shell metacharacters, it could potentially cause issues. The current regex patterns (^[0-9]+\.[0-9]+\.[0-9]+$ and ^devnet[0-9]+$) are strict enough to prevent shell injection, but consider adding explicit validation that the extracted values contain only expected characters before using them in git commands and other shell operations.

Copilot uses AI. Check for mistakes.

# Check for mandatory labels - both version and devnet are required
MISSING_LABELS=""

if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "ℹ️ No version label found"
else
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "git_tag=v$VERSION" >> $GITHUB_OUTPUT
echo "✅ Version found: $VERSION"
MISSING_LABELS="version label (e.g., 1.0.0)"
fi

if [ -n "$ZEAM_TAG" ] && [ "$ZEAM_TAG" != "null" ]; then
echo "zeam_tag=$ZEAM_TAG" >> $GITHUB_OUTPUT
echo "has_network_tag=true" >> $GITHUB_OUTPUT
echo "✅ Found network tag: $ZEAM_TAG"
else
echo "has_network_tag=false" >> $GITHUB_OUTPUT
echo "ℹ️ No network tag found (optional)"
if [ -z "$DEVNET_LABEL" ] || [ "$DEVNET_LABEL" = "null" ]; then
if [ -n "$MISSING_LABELS" ]; then
MISSING_LABELS="$MISSING_LABELS and devnet label (devnet0, devnet1, devnet2, etc.)"
else
MISSING_LABELS="devnet label (devnet0, devnet1, devnet2, etc.)"
fi
fi

# Require at least one label (version or network)
if { [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; } && { [ -z "$ZEAM_TAG" ] || [ "$ZEAM_TAG" = "null" ]; }; then
echo "❌ No usable label found! Please add a version (e.g. v1.0.0) or network tag (e.g. devnet0, testnet, mainnet)"
# Exit if any required labels are missing
if [ -n "$MISSING_LABELS" ]; then
echo "❌ Missing required labels: $MISSING_LABELS"
echo "Please add all required labels to proceed with the release."
exit 1
fi

- name: Create and push git tags
id: create_tags
# Check if version tag already exists (add 'v' prefix for git tag)
VERSION_TAG="v$VERSION"
if git rev-parse "$VERSION_TAG" >/dev/null 2>&1; then
echo "❌ Version tag $VERSION_TAG already exists. Please use a new version."
exit 1
fi

# Create proper naming to avoid branch/tag conflicts
DEVNET_TAG="$(echo $DEVNET_LABEL | sed 's/^d/D/')" # devnet1 -> Devnet1
DEVNET_BRANCH="$DEVNET_LABEL" # devnet1

# Set outputs only after validation passes
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "git_tag=$VERSION_TAG" >> $GITHUB_OUTPUT
echo "devnet_label=$DEVNET_LABEL" >> $GITHUB_OUTPUT
echo "devnet_tag=$DEVNET_TAG" >> $GITHUB_OUTPUT
echo "devnet_branch=$DEVNET_BRANCH" >> $GITHUB_OUTPUT
echo "✅ Found version: $VERSION and devnet label: $DEVNET_LABEL"
echo "✅ Will create branch: $DEVNET_BRANCH and tag: $DEVNET_TAG"

echo "✅ All required labels validated"

- name: Delete existing devnet tag and release if present
run: |
DEVNET_TAG="${{ steps.get_labels.outputs.devnet_tag }}"

# Check if tag exists and delete it
if git rev-parse "$DEVNET_TAG" >/dev/null 2>&1; then
echo "🗑️ Deleting existing tag $DEVNET_TAG"
git tag -d "$DEVNET_TAG" 2>/dev/null || true
git push origin --delete "$DEVNET_TAG" 2>/dev/null || true
echo "✅ Deleted existing tag $DEVNET_TAG"
else
echo "ℹ️ Tag $DEVNET_TAG does not exist"
fi

# Delete GitHub release if it exists
echo "🗑️ Attempting to delete existing GitHub release $DEVNET_TAG"
gh release delete "$DEVNET_TAG" --yes 2>/dev/null || echo "ℹ️ Release $DEVNET_TAG does not exist or already deleted"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create/Update devnet branch
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

CREATE_VERSION_TAG=false
CREATE_NETWORK_TAG=false
DEVNET_BRANCH="${{ steps.get_labels.outputs.devnet_branch }}"

# Check if version tag should be created
if [ -n "${{ steps.get_tags_labels.outputs.version }}" ] && [ "${{ steps.get_tags_labels.outputs.version }}" != "null" ]; then
if git rev-parse "${{ steps.get_tags_labels.outputs.git_tag }}" >/dev/null 2>&1; then
echo "❌ Version tag ${{ steps.get_tags_labels.outputs.git_tag }} already exists. Please create a new tag."
exit 1
else
CREATE_VERSION_TAG=true
fi
# Check if devnet branch already exists remotely
if git ls-remote --heads origin "$DEVNET_BRANCH" | grep -q "$DEVNET_BRANCH"; then
echo "ℹ️ Branch $DEVNET_BRANCH already exists remotely"
git checkout "$DEVNET_BRANCH"
git pull origin "$DEVNET_BRANCH"
else
echo "✅ Creating new branch $DEVNET_BRANCH from main"
git checkout -b "$DEVNET_BRANCH"
fi

Comment on lines +110 to 116
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When checking out an existing devnet branch from remote, the code doesn't ensure it's starting from the correct commit state. If the remote devnet branch has diverged from main significantly, or if this is being run after a force push or other Git history manipulation, the merge operation could produce unexpected results. Consider adding a check to verify the devnet branch's relationship to main, or document the expected branch topology.

Suggested change
git checkout "$DEVNET_BRANCH"
git pull origin "$DEVNET_BRANCH"
else
echo "✅ Creating new branch $DEVNET_BRANCH from main"
git checkout -b "$DEVNET_BRANCH"
fi
# Ensure local devnet branch matches the remote state
git fetch origin "$DEVNET_BRANCH"
git checkout -B "$DEVNET_BRANCH" "origin/$DEVNET_BRANCH"
else
echo "✅ Creating new branch $DEVNET_BRANCH from main"
# Base new devnet branch on the latest origin/main
git fetch origin main
git checkout -B "$DEVNET_BRANCH" origin/main
fi
# Ensure local main is up to date with origin/main before merging
git fetch origin main:main

Copilot uses AI. Check for mistakes.
# Check if network tag should be created
if [ "${{ steps.get_tags_labels.outputs.has_network_tag }}" = "true" ]; then
ZEAM_GIT_TAG="${{ steps.get_tags_labels.outputs.zeam_tag }}"
if git rev-parse "$ZEAM_GIT_TAG" >/dev/null 2>&1; then
echo "❌ Network tag $ZEAM_GIT_TAG already exists. Please create a new tag."
exit 1
else
CREATE_NETWORK_TAG=true
fi
fi
# Merge latest main changes into devnet branch
git merge main --no-ff -m "Merge main into $DEVNET_BRANCH for deployment"
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The merge operation may fail if there are conflicts between the main branch and the existing devnet branch. This scenario is not handled, and the workflow will fail without a clear resolution path. Consider adding conflict detection and appropriate error messaging to guide users on how to resolve conflicts manually.

Copilot uses AI. Check for mistakes.
git push -u origin "$DEVNET_BRANCH"
echo "✅ Updated branch $DEVNET_BRANCH with latest main changes"

# Create version tag if needed
if [ "$CREATE_VERSION_TAG" = "true" ]; then
git tag -a "${{ steps.get_tags_labels.outputs.git_tag }}" -m "Release version ${{ steps.get_tags_labels.outputs.version }}"
git push origin "${{ steps.get_tags_labels.outputs.git_tag }}"
echo "✅ Created version tag ${{ steps.get_tags_labels.outputs.git_tag }}"
# Switch back to main for tagging
git checkout main

- name: Generate changelog
id: changelog
run: |
# Get the last devnet tag for changelog comparison
CURRENT_DEVNET_TAG="${{ steps.get_labels.outputs.devnet_tag }}"
DEVNET_TAG_PREFIX="$(echo ${{ steps.get_labels.outputs.devnet_label }} | sed 's/^d/D/')"
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tag prefix calculation duplicates the logic from line 66 but is calculated again here. This duplication could lead to maintenance issues if the naming convention changes. Consider reusing the devnet_tag output from the get_labels step instead of recalculating it.

Suggested change
DEVNET_TAG_PREFIX="$(echo ${{ steps.get_labels.outputs.devnet_label }} | sed 's/^d/D/')"
VERSION="${{ steps.get_labels.outputs.version }}"
DEVNET_TAG_PREFIX="${CURRENT_DEVNET_TAG%-${VERSION}}"

Copilot uses AI. Check for mistakes.
LAST_DEVNET_TAG=$(git tag -l "${DEVNET_TAG_PREFIX}*" --sort=-version:refname | grep -v "^$CURRENT_DEVNET_TAG$" | head -n 1)
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow filters tags using version:refname sort, which may not work correctly for tags like "Devnet1", "Devnet2", "Devnet10", etc. This sorting method is designed for semantic version numbers and may produce unexpected results with the "DevnetX" naming convention. For example, "Devnet10" might be sorted before "Devnet2". Consider using a different sorting mechanism or document this limitation.

Suggested change
LAST_DEVNET_TAG=$(git tag -l "${DEVNET_TAG_PREFIX}*" --sort=-version:refname | grep -v "^$CURRENT_DEVNET_TAG$" | head -n 1)
LAST_DEVNET_TAG=$(git tag -l "${DEVNET_TAG_PREFIX}*" --sort=-creatordate | grep -v "^$CURRENT_DEVNET_TAG$" | head -n 1)

Copilot uses AI. Check for mistakes.

if [ -z "$LAST_DEVNET_TAG" ]; then
echo "ℹ️ No previous devnet tag found, generating changelog from last 20 commits"
# Get commits with author and PR info for GitHub-style format
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" | head -20 | sed 's/ by @/ by @/g' | sed 's/ in / in #/g')
else
echo "ℹ️ Generating changelog from $LAST_DEVNET_TAG to current"
# Get commits between tags with author and PR info
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" $LAST_DEVNET_TAG..HEAD | sed 's/ by @/ by @/g' | sed 's/ in / in #/g')
Comment on lines +135 to +140
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sed substitutions being used here are redundant and incorrect. The pattern 's/ by @/ by @/g' replaces " by @" with " by @" (same text), and 's/ in / in #/g' replaces " in " with " in #" which doesn't properly prepend '#' to commit hashes. The intent appears to be formatting the commit hash with '#', but this will result in text like "in #abc123" instead of proper GitHub commit references. Consider using the proper format string in git log or adjusting the sed pattern to correctly format the output.

Suggested change
# Get commits with author and PR info for GitHub-style format
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" | head -20 | sed 's/ by @/ by @/g' | sed 's/ in / in #/g')
else
echo "ℹ️ Generating changelog from $LAST_DEVNET_TAG to current"
# Get commits between tags with author and PR info
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" $LAST_DEVNET_TAG..HEAD | sed 's/ by @/ by @/g' | sed 's/ in / in #/g')
# Get commits with author and hash info for GitHub-style format
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" | head -20)
else
echo "ℹ️ Generating changelog from $LAST_DEVNET_TAG to current"
# Get commits between tags with author and hash info
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" $LAST_DEVNET_TAG..HEAD)

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +140
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as line 136 - the sed substitutions are redundant and incorrect. The pattern 's/ by @/ by @/g' doesn't change anything, and 's/ in / in #/g' doesn't properly format commit references for GitHub.

Suggested change
# Get commits with author and PR info for GitHub-style format
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" | head -20 | sed 's/ by @/ by @/g' | sed 's/ in / in #/g')
else
echo "ℹ️ Generating changelog from $LAST_DEVNET_TAG to current"
# Get commits between tags with author and PR info
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" $LAST_DEVNET_TAG..HEAD | sed 's/ by @/ by @/g' | sed 's/ in / in #/g')
# Get commits with author and commit info for GitHub-style format
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" | head -20)
else
echo "ℹ️ Generating changelog from $LAST_DEVNET_TAG to current"
# Get commits between tags with author and commit info
CHANGELOG=$(git log --oneline --pretty=format:"- %s by @%an in %h" $LAST_DEVNET_TAG..HEAD)

Copilot uses AI. Check for mistakes.
fi

# Create network tag if needed
if [ "$CREATE_NETWORK_TAG" = "true" ]; then
git tag -a "$ZEAM_GIT_TAG" -m "Zeam network tag for $ZEAM_GIT_TAG"
git push origin "$ZEAM_GIT_TAG"
echo "✅ Created zeam tag $ZEAM_GIT_TAG"
# If no commits found, add a default message
if [ -z "$CHANGELOG" ]; then
CHANGELOG="- No changes found in this release"
fi
echo "create_version_tag=$CREATE_VERSION_TAG" >> $GITHUB_OUTPUT
echo "create_network_tag=$CREATE_NETWORK_TAG" >> $GITHUB_OUTPUT

# Create release notes with GitHub-style changelog
cat > release_notes.md << EOF
# Release Notes

**Network:** ${{ steps.get_labels.outputs.devnet_label }}
**Version:** ${{ steps.get_labels.outputs.version }}
**Tag:** ${{ steps.get_labels.outputs.devnet_tag }}
**Branch:** ${{ steps.get_labels.outputs.devnet_branch }}
**Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")

## What's Changed

${CHANGELOG}

Comment on lines +160 to +161
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The heredoc (lines 149-164) uses variable interpolation with GitHub Actions expressions like ${{ steps.get_labels.outputs.devnet_label }}. However, if the CHANGELOG variable contains special characters or formatting, it could break the heredoc structure or produce unexpected output. Consider using a different approach to write the release notes, such as using proper quoting or writing the file programmatically with echo statements that properly escape content.

Suggested change
${CHANGELOG}
EOF
# Append the changelog content safely without further shell expansion
printf '%s\n' "$CHANGELOG" >> release_notes.md
printf '\n' >> release_notes.md
cat >> release_notes.md << EOF

Copilot uses AI. Check for mistakes.
**Full Changelog**: https://github.com/${{ github.repository }}/commits/${{ steps.get_labels.outputs.devnet_tag }}

EOF

echo "✅ Generated changelog with $(echo "$CHANGELOG" | wc -l) commits"
echo "changelog_file=release_notes.md" >> $GITHUB_OUTPUT

- name: Create and push tags on main
id: create_tags
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

VERSION_TAG="${{ steps.get_labels.outputs.git_tag }}"
DEVNET_TAG="${{ steps.get_labels.outputs.devnet_tag }}"

# Create version tag on main
git tag -a "$VERSION_TAG" -m "Release version ${{ steps.get_labels.outputs.version }}"
git push origin "$VERSION_TAG"
echo "✅ Created version tag $VERSION_TAG on main branch"

# Create devnet tag on main (already deleted if existed)
git tag -a "$DEVNET_TAG" -m "Devnet deployment tag for $DEVNET_TAG"
git push origin "$DEVNET_TAG"
echo "✅ Created devnet tag $DEVNET_TAG on main branch"

- name: Create GitHub Release
if: ${{ steps.create_tags.outputs.create_version_tag == 'true' || steps.create_tags.outputs.create_network_tag == 'true' }}
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.get_tags_labels.outputs.git_tag != '' && steps.get_tags_labels.outputs.git_tag != 'null' && steps.get_tags_labels.outputs.git_tag || steps.get_tags_labels.outputs.zeam_tag }}
name: Release ${{ steps.get_tags_labels.outputs.git_tag != '' && steps.get_tags_labels.outputs.git_tag != 'null' && steps.get_tags_labels.outputs.git_tag || steps.get_tags_labels.outputs.zeam_tag }}
body_path: ./README.md
tag_name: ${{ steps.get_labels.outputs.devnet_tag }}
name: Release ${{ steps.get_labels.outputs.version }} - ${{ steps.get_labels.outputs.devnet_tag }}
body_path: ${{ steps.changelog.outputs.changelog_file }}
draft: false
prerelease: false
prerelease: true
target_commitish: main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Loading