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
146 changes: 146 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
name: Prepare Release

on:
workflow_dispatch:
inputs:
crate:
description: 'Crate to release'
required: true
type: choice
options:
- sentinel
- contributor-rewards
- doublezero-solana
- solana-validator-debt
version:
description: 'Version (e.g., 0.2.1 or 0.2.0-rc1)'
required: true
type: string

permissions:
contents: write
pull-requests: write

jobs:
prepare-release:
runs-on: ubuntu-latest
steps:
- name: Validate version format
run: |
if ! echo "${{ github.event.inputs.version }}" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$'; then
echo "[ERROR] Invalid version format. Expected: X.Y.Z or X.Y.Z-rcN (where N is a number)"
Comment on lines +30 to +31
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

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

The regex pattern requires exactly 3 version components (X.Y.Z), but semantic versioning also allows 2-component versions (X.Y) which may be needed. Consider if the pattern should support both formats or if the strict 3-component requirement is intentional.

Suggested change
if ! echo "${{ github.event.inputs.version }}" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$'; then
echo "[ERROR] Invalid version format. Expected: X.Y.Z or X.Y.Z-rcN (where N is a number)"
if ! echo "${{ github.event.inputs.version }}" | grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?(-rc[0-9]+)?$'; then
echo "[ERROR] Invalid version format. Expected: X.Y, X.Y-rcN, X.Y.Z, or X.Y.Z-rcN (where N is a number)"

Copilot uses AI. Check for mistakes.
exit 1
fi

- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Install git-cliff
uses: taiki-e/install-action@git-cliff

- name: Set up crate metadata
id: crate_meta
run: |
case "${{ github.event.inputs.crate }}" in
sentinel)
echo "crate_path=crates/sentinel" >> $GITHUB_OUTPUT
echo "tag_prefix=sentinel" >> $GITHUB_OUTPUT
echo "crate_name=sentinel" >> $GITHUB_OUTPUT
;;
contributor-rewards)
echo "crate_path=crates/contributor-rewards" >> $GITHUB_OUTPUT
echo "tag_prefix=contributor-rewards" >> $GITHUB_OUTPUT
echo "crate_name=contributor-rewards" >> $GITHUB_OUTPUT
;;
doublezero-solana)
echo "crate_path=crates/solana-cli" >> $GITHUB_OUTPUT
echo "tag_prefix=doublezero-solana" >> $GITHUB_OUTPUT
echo "crate_name=doublezero-solana" >> $GITHUB_OUTPUT
;;
solana-validator-debt)
echo "crate_path=crates/validator-debt" >> $GITHUB_OUTPUT
echo "tag_prefix=solana-validator-debt" >> $GITHUB_OUTPUT
echo "crate_name=solana-validator-debt" >> $GITHUB_OUTPUT
;;
*)
echo "[ERROR] Unknown crate: ${{ github.event.inputs.crate }}"
exit 1
;;
esac

- name: Find previous tag
id: prev_tag
run: |
TAG_PREFIX="${{ steps.crate_meta.outputs.tag_prefix }}"
PREV_TAG=$(git tag --list "${TAG_PREFIX}/v*" --sort=-v:refname | head -n 1)

if [ -z "$PREV_TAG" ]; then
echo "[WARN] No previous tag found for ${TAG_PREFIX}, using first commit"
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
fi

echo "prev_tag=${PREV_TAG}" >> $GITHUB_OUTPUT
echo "[OK] Previous tag: ${PREV_TAG}"

- name: Generate changelog
run: |
TAG_PREFIX="${{ steps.crate_meta.outputs.tag_prefix }}"
VERSION="${{ github.event.inputs.version }}"
PREV_TAG="${{ steps.prev_tag.outputs.prev_tag }}"
CHANGELOG_PATH="${{ steps.crate_meta.outputs.crate_path }}/CHANGELOG.md"

echo "[OK] Generating changelog for ${TAG_PREFIX}/v${VERSION}"
echo "[OK] Range: ${PREV_TAG}..HEAD"

# Generate new changelog entry
git cliff \
--config cliff.toml \
--tag-pattern "${TAG_PREFIX}/v[0-9]*" \
--tag "${TAG_PREFIX}/v${VERSION}" \
--unreleased \
--prepend "${CHANGELOG_PATH}" \
"${PREV_TAG}..HEAD"

echo "[OK] Changelog updated at ${CHANGELOG_PATH}"

# Show diff for verification
git diff "${CHANGELOG_PATH}"

- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: |
chore(${{ steps.crate_meta.outputs.crate_name }}): prepare v${{ github.event.inputs.version }} release

Update CHANGELOG.md for ${{ steps.crate_meta.outputs.crate_name }} v${{ github.event.inputs.version }}
branch: release/${{ steps.crate_meta.outputs.crate_name }}-v${{ github.event.inputs.version }}
delete-branch: true
title: "chore(${{ steps.crate_meta.outputs.crate_name }}): prepare v${{ github.event.inputs.version }} release"
body: |
## Release Preparation for ${{ steps.crate_meta.outputs.crate_name }} v${{ github.event.inputs.version }}

This PR updates the CHANGELOG.md for the upcoming release.

### Next Steps
1. Review the changelog updates
2. Merge this PR to main
3. Create and push the release tag:
```bash
git tag ${{ steps.crate_meta.outputs.tag_prefix }}/v${{ github.event.inputs.version }}
git push origin ${{ steps.crate_meta.outputs.tag_prefix }}/v${{ github.event.inputs.version }}
```
4. The existing goreleaser workflow will automatically build and publish the release

### Changes
- Updated `${{ steps.crate_meta.outputs.crate_path }}/CHANGELOG.md`

---
*This PR was automatically generated by the [Prepare Release workflow](.github/workflows/prepare-release.yml)*
labels: |
release
changelog
assignees: ${{ github.actor }}
100 changes: 100 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
[changelog]
# Changelog header
header = """
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
"""

# Template for the changelog body
body = """
{% if version %}\
## [{{ version | split(pat="/") | last | trim_start_matches(pat="v") }}](https://github.com/doublezerofoundation/doublezero-offchain/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [Unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.message | split(pat="\n") | first | trim }}
{% endfor %}
{% endfor %}

"""

# Template for the changelog footer (remove previous unreleased section)
footer = ""

# Remove previous unreleased section when generating new release
trim = true

[git]
# Parse commits based on conventional format, but include all commits
conventional_commits = true
filter_unconventional = false
split_commits = false

# Protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false

# Filter commits by matching regex
filter_commits = false

# Tag pattern for matching tags
tag_pattern = "v[0-9]*"
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

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

The tag_pattern 'v[0-9]' doesn't match the monorepo tag format used in the workflow ('{crate}/v{version}'). This should be updated to match the actual tag pattern, such as '/v[0-9]*' or left more permissive, otherwise git-cliff may not correctly identify tags for changelog generation.

Suggested change
tag_pattern = "v[0-9]*"
tag_pattern = "*/v[0-9]*"

Copilot uses AI. Check for mistakes.

# Skip tags matching this pattern
skip_tags = ""

# Ignore tags that don't follow semver
ignore_tags = ""

# Date order for sorting tags
topo_order = false
sort_commits = "oldest"

# Limit number of commits included in changelog
limit_commits = 0

# Commit preprocessors (regex-based modifications before parsing)
commit_preprocessors = [
# Remove issue numbers from commit messages (currently disabled)
# { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
]

# Commit parsers to categorize commits
commit_parsers = [
# Conventional commit parsers
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Fixed" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactoring" },
# Skip maintenance and non-user-facing commits
{ message = "^version [0-9]", skip = true },
{ message = "^style", skip = true },
{ message = "^test", skip = true },
{ message = "^chore\\(release\\)", skip = true },
{ message = "^chore\\(changelog\\)", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore", skip = true },
{ message = "^ci", skip = true },
{ message = "^docs", skip = true },
{ message = "^build", skip = true },
# Catch-all for everything else
{ message = ".*", group = "Other" },
]

# Link parsers to extract references
link_parsers = [
# Parse GitHub issue/PR references
{ pattern = "#(\\d+)", href = "https://github.com/doublezerofoundation/doublezero-offchain/pull/$1" },
]

[remote.github]
owner = "doublezerofoundation"
repo = "doublezero-offchain"