mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-12 00:28:20 -04:00
feat(ci): overhaul changelog management and PR auto-labeling (#5254)
This commit is contained in:
71
.github/workflows/main-push-changelog.yml
vendored
71
.github/workflows/main-push-changelog.yml
vendored
@@ -1,71 +0,0 @@
|
||||
name: Main Push Changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: main-push-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
main-push-changelog:
|
||||
name: Generate main push changelog
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine last tag
|
||||
id: last_prod_tag
|
||||
run: |
|
||||
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||
echo "Found last tag: $TAG"
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate changelog from last tag to current
|
||||
if: steps.last_prod_tag.outputs.tag != ''
|
||||
uses: mikepenz/release-changelog-builder-action@v6
|
||||
id: changelog
|
||||
with:
|
||||
configuration: .github/release.yml
|
||||
fromTag: ${{ steps.last_prod_tag.outputs.tag }}
|
||||
toTag: ${{ github.sha }}
|
||||
outputFile: main-push-changelog.md
|
||||
fetchViaCommits: true
|
||||
fetchReviewers: false
|
||||
fetchReleaseInformation: false
|
||||
fetchReviews: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload changelog artifact
|
||||
if: steps.last_prod_tag.outputs.tag != ''
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: main-push-changelog
|
||||
path: main-push-changelog.md
|
||||
|
||||
- name: Print main push summary
|
||||
env:
|
||||
LAST_TAG: ${{ steps.last_prod_tag.outputs.tag }}
|
||||
run: |
|
||||
echo "Pushed to main"
|
||||
echo "SHA: $GITHUB_SHA"
|
||||
echo "Actor: $GITHUB_ACTOR"
|
||||
echo "Ref: $GITHUB_REF"
|
||||
echo ""
|
||||
if [ "$LAST_TAG" != "" ]; then
|
||||
echo "Changelog since last tag ($LAST_TAG)":
|
||||
echo "----------------------------------------"
|
||||
cat main-push-changelog.md
|
||||
else
|
||||
echo "No tag found. Skipping changelog generation."
|
||||
fi
|
||||
36
.github/workflows/models_pr_triage.yml
vendored
36
.github/workflows/models_pr_triage.yml
vendored
@@ -2,7 +2,7 @@ name: PR Triage (Models)
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const skipLabels = new Set(['automation', 'release']);
|
||||
const typeLabels = new Set(['bugfix', 'enhancement', 'dependencies', 'repo', 'refactor']);
|
||||
const typeLabels = new Set(['bugfix', 'enhancement', 'dependencies', 'repo', 'refactor', 'chore', 'ci', 'build', 'testing', 'documentation']);
|
||||
const prLabels = context.payload.pull_request.labels.map(l => l.name);
|
||||
|
||||
const shouldSkipAll = prLabels.some(l => skipLabels.has(l));
|
||||
@@ -105,15 +105,26 @@ jobs:
|
||||
prompt: |
|
||||
Classify this pull request for the Meshtastic Android app into exactly one category.
|
||||
|
||||
Return exactly one of: bugfix, enhancement, refactor
|
||||
Return exactly one of: bugfix, enhancement, refactor, chore, ci, build, testing, documentation
|
||||
|
||||
Use bugfix if it fixes a bug, crash, or incorrect behavior.
|
||||
Use enhancement if it adds a new feature, improves performance, or adds new functionality.
|
||||
Use refactor if it restructures code without changing behavior, cleans up code, or improves architecture.
|
||||
Label definitions:
|
||||
- bugfix: Fixes a bug, crash, or incorrect behavior
|
||||
- enhancement: Adds a new feature, improves UX, adds new functionality, or improves performance
|
||||
- refactor: Restructures code without changing behavior, cleans up code, or improves architecture
|
||||
- chore: Routine maintenance (dependency updates, config tweaks, version bumps) that doesn't change app behavior
|
||||
- ci: Changes to CI/CD workflows, GitHub Actions, or automation scripts
|
||||
- build: Changes to build system, Gradle config, build-logic, or compilation settings
|
||||
- testing: Adds or modifies tests without changing production code
|
||||
- documentation: Documentation-only changes (README, CHANGELOG, code comments, KDoc)
|
||||
|
||||
Title: ${{ env.PR_TITLE }}
|
||||
Body: ${{ env.PR_BODY }}
|
||||
system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label.
|
||||
system-prompt: >
|
||||
You classify pull requests into categories for changelog generation.
|
||||
Pick the single most specific label. Prefer ci/build/chore/testing/documentation
|
||||
over refactor when the change clearly fits those narrower categories.
|
||||
Only use bugfix for actual bug fixes, not for CI fixes or build fixes.
|
||||
Only use enhancement for user-facing features, not for internal improvements.
|
||||
model: openai/gpt-4o-mini
|
||||
|
||||
- name: Apply type label
|
||||
@@ -125,9 +136,14 @@ jobs:
|
||||
script: |
|
||||
const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
|
||||
const labelMeta = {
|
||||
'bugfix': { color: 'd73a4a', description: 'Bug fix' },
|
||||
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
|
||||
'refactor': { color: 'c5def5', description: 'Code restructuring without behavior change' },
|
||||
'bugfix': { color: 'd73a4a', description: 'Bug fix' },
|
||||
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
|
||||
'refactor': { color: 'c5def5', description: 'Code restructuring without behavior change' },
|
||||
'chore': { color: 'ededed', description: 'Routine maintenance' },
|
||||
'ci': { color: 'bfd4f2', description: 'CI/CD and automation changes' },
|
||||
'build': { color: 'bfd4f2', description: 'Build system changes' },
|
||||
'testing': { color: 'f9d0c4', description: 'Test additions or modifications' },
|
||||
'documentation': { color: '0075ca', description: 'Documentation changes' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
||||
2
.github/workflows/pr_enforce_labels.yml
vendored
2
.github/workflows/pr_enforce_labels.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
script: |
|
||||
// Extract labels from the payload directly to avoid extra API calls
|
||||
const latestLabels = context.payload.pull_request.labels.map(label => label.name);
|
||||
const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor'];
|
||||
const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor', 'chore', 'ci', 'build', 'testing', 'documentation'];
|
||||
console.log('Labels from payload:', latestLabels);
|
||||
const hasRequiredLabel = latestLabels.some(label => requiredLabels.includes(label));
|
||||
if (!hasRequiredLabel) {
|
||||
|
||||
66
.github/workflows/promote.yml
vendored
66
.github/workflows/promote.yml
vendored
@@ -142,6 +142,72 @@ jobs:
|
||||
--draft=false \
|
||||
--prerelease=${{ inputs.channel != 'production' }}
|
||||
|
||||
- name: Stamp CHANGELOG.md for release
|
||||
if: ${{ inputs.channel == 'production' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ inputs.final_tag }}
|
||||
VERSION: ${{ inputs.base_version }}
|
||||
run: |
|
||||
DATE=$(date -u +%Y-%m-%d)
|
||||
|
||||
# Checkout CHANGELOG.md from main (we're on a tag checkout)
|
||||
git fetch origin main
|
||||
git checkout -b "changelog/v${VERSION}" origin/main
|
||||
|
||||
# Move the [Unreleased] content into a versioned section
|
||||
python3 -c "
|
||||
import sys
|
||||
version, date = sys.argv[1], sys.argv[2]
|
||||
with open('CHANGELOG.md', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
us = '<!-- UNRELEASED_START -->'
|
||||
ue = '<!-- UNRELEASED_END -->'
|
||||
rs = '<!-- RELEASED_START -->'
|
||||
|
||||
start = content.find(us)
|
||||
end = content.find(ue)
|
||||
rstart = content.find(rs)
|
||||
|
||||
if start == -1 or end == -1 or rstart == -1:
|
||||
print('Markers not found in CHANGELOG.md')
|
||||
sys.exit(1)
|
||||
|
||||
# Extract unreleased content (between markers, excluding the ## [Unreleased] header and 'Changes since' line)
|
||||
unreleased = content[start + len(us):end].strip()
|
||||
lines = unreleased.split('\n')
|
||||
# Skip the header and the 'Changes since' reference line
|
||||
body_lines = []
|
||||
for line in lines:
|
||||
if line.startswith('## [Unreleased]'):
|
||||
continue
|
||||
if line.startswith('Changes since'):
|
||||
continue
|
||||
body_lines.append(line)
|
||||
body = '\n'.join(body_lines).strip()
|
||||
|
||||
# Build versioned entry
|
||||
versioned = f'## [{version}] - {date}\n\n{body}'
|
||||
|
||||
# Reset unreleased section
|
||||
fresh_unreleased = '## [Unreleased]\n\n*No changes yet.*'
|
||||
|
||||
# Replace unreleased with fresh, insert versioned before RELEASED_START
|
||||
new_content = content[:start + len(us)] + '\n' + fresh_unreleased + '\n' + content[end:rstart + len(rs)] + '\n\n' + versioned + '\n' + content[rstart + len(rs):]
|
||||
|
||||
with open('CHANGELOG.md', 'w') as f:
|
||||
f.write(new_content)
|
||||
" "$VERSION" "$DATE"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add CHANGELOG.md
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "docs: release CHANGELOG.md for v${VERSION} [skip ci]"
|
||||
git push origin "changelog/v${VERSION}":main
|
||||
}
|
||||
|
||||
- name: Notify Discord
|
||||
if: ${{ inputs.channel != 'internal' }}
|
||||
env:
|
||||
|
||||
49
.github/workflows/pull-request-target.yml
vendored
49
.github/workflows/pull-request-target.yml
vendored
@@ -21,22 +21,48 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const branch = context.payload.pull_request.head.ref;
|
||||
const title = context.payload.pull_request.title || '';
|
||||
const labels = new Set();
|
||||
|
||||
// enhancement: branch contains feat
|
||||
if (/feat/i.test(branch)) labels.add('enhancement');
|
||||
// Match branch prefix (e.g. feat/..., fix/..., chore-something)
|
||||
// Also parse conventional-commit-style PR titles as a fallback
|
||||
const branchPrefix = branch.split(/[\/\-_]/)[0].toLowerCase();
|
||||
const titlePrefix = (title.match(/^(\w+)[\(:]/) || [])[1]?.toLowerCase();
|
||||
|
||||
// bugfix: branch starts with fix or bug
|
||||
if (/^(fix|bug)/i.test(branch)) labels.add('bugfix');
|
||||
const prefix = branchPrefix || titlePrefix;
|
||||
|
||||
// refactor: branch starts with refactor
|
||||
if (/^refactor/i.test(branch)) labels.add('refactor');
|
||||
// Map prefixes to changelog-aligned labels
|
||||
const prefixMap = {
|
||||
'feat': 'enhancement',
|
||||
'feature': 'enhancement',
|
||||
'fix': 'bugfix',
|
||||
'bug': 'bugfix',
|
||||
'bugfix': 'bugfix',
|
||||
'hotfix': 'bugfix',
|
||||
'refactor': 'refactor',
|
||||
'chore': 'chore',
|
||||
'build': 'build',
|
||||
'ci': 'ci',
|
||||
'test': 'testing',
|
||||
'tests': 'testing',
|
||||
'docs': 'documentation',
|
||||
'doc': 'documentation',
|
||||
'perf': 'enhancement',
|
||||
'style': 'refactor',
|
||||
'repo': 'repo',
|
||||
'release': 'release',
|
||||
};
|
||||
|
||||
// repo: branch contains repo or ci
|
||||
if (/repo|ci/i.test(branch)) {
|
||||
labels.add('repo');
|
||||
} else {
|
||||
// Also label 'repo' if .github files were changed (needs one API call)
|
||||
// Label from branch prefix
|
||||
if (prefixMap[branchPrefix]) labels.add(prefixMap[branchPrefix]);
|
||||
|
||||
// Label from PR title prefix (if different from branch)
|
||||
if (titlePrefix && prefixMap[titlePrefix] && !labels.has(prefixMap[titlePrefix])) {
|
||||
labels.add(prefixMap[titlePrefix]);
|
||||
}
|
||||
|
||||
// Also label 'repo' if .github/ or build-logic/ files were changed
|
||||
if (!labels.has('repo') && !labels.has('ci') && !labels.has('build')) {
|
||||
try {
|
||||
const files = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
@@ -44,6 +70,7 @@ jobs:
|
||||
(res) => res.data.map(f => f.filename)
|
||||
);
|
||||
if (files.some(f => f.startsWith('.github/'))) labels.add('repo');
|
||||
if (files.some(f => f.startsWith('build-logic/'))) labels.add('build');
|
||||
} catch (e) {
|
||||
core.warning(`Could not list PR files (rate limited?): ${e.message}`);
|
||||
}
|
||||
|
||||
104
.github/workflows/update-changelog.yml
vendored
Normal file
104
.github/workflows/update-changelog.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Update Changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'CHANGELOG.md'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: changelog-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-changelog:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine last production tag
|
||||
id: last_tag
|
||||
run: |
|
||||
# Find the latest production tag (vX.Y.Z without pre-release suffix)
|
||||
TAG=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
|
||||
echo "Found last production tag: ${TAG:-none}"
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate release notes via GitHub API
|
||||
if: steps.last_tag.outputs.tag != ''
|
||||
id: notes
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ steps.last_tag.outputs.tag }}
|
||||
run: |
|
||||
# Use GitHub's native release notes generator (same engine as generate_release_notes)
|
||||
NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
|
||||
-f tag_name="unreleased" \
|
||||
-f target_commitish="${{ github.sha }}" \
|
||||
-f previous_tag_name="$TAG" \
|
||||
--jq '.body')
|
||||
|
||||
# Strip GitHub comment headers and the "What's Changed" heading
|
||||
NOTES=$(echo "$NOTES" | sed '/^<!-- .* -->/d' | sed '/^## What'\''s Changed$/d')
|
||||
# Strip the trailing "Full Changelog" link
|
||||
NOTES=$(echo "$NOTES" | sed '/^\*\*Full Changelog\*\*/d')
|
||||
# Strip leading and trailing blank lines
|
||||
NOTES=$(echo "$NOTES" | sed '/./,$!d' | sed -e :a -e '/^\n*$/{$d;N;ba}')
|
||||
|
||||
# Write to file for next step (avoids shell quoting issues)
|
||||
echo "$NOTES" > /tmp/unreleased-notes.md
|
||||
|
||||
- name: Update CHANGELOG.md
|
||||
if: steps.last_tag.outputs.tag != ''
|
||||
env:
|
||||
TAG: ${{ steps.last_tag.outputs.tag }}
|
||||
run: |
|
||||
NOTES=$(cat /tmp/unreleased-notes.md)
|
||||
|
||||
# Build the new unreleased section
|
||||
UNRELEASED=$(cat <<SECTION
|
||||
## [Unreleased]
|
||||
|
||||
Changes since [\`${TAG}\`](https://github.com/${{ github.repository }}/releases/tag/${TAG}):
|
||||
|
||||
${NOTES}
|
||||
SECTION
|
||||
)
|
||||
|
||||
# Replace content between UNRELEASED markers
|
||||
python3 -c "
|
||||
import sys
|
||||
start_marker = '<!-- UNRELEASED_START -->'
|
||||
end_marker = '<!-- UNRELEASED_END -->'
|
||||
section = sys.argv[1]
|
||||
with open('CHANGELOG.md', 'r') as f:
|
||||
content = f.read()
|
||||
start = content.find(start_marker)
|
||||
end = content.find(end_marker)
|
||||
if start == -1 or end == -1:
|
||||
print('Markers not found in CHANGELOG.md')
|
||||
sys.exit(1)
|
||||
new_content = content[:start + len(start_marker)] + '\n' + section + '\n' + content[end:]
|
||||
with open('CHANGELOG.md', 'w') as f:
|
||||
f.write(new_content)
|
||||
" "$UNRELEASED"
|
||||
|
||||
- name: Commit and push
|
||||
if: steps.last_tag.outputs.tag != ''
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add CHANGELOG.md
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "docs: update CHANGELOG.md [skip ci]"
|
||||
git push origin main
|
||||
}
|
||||
14
CHANGELOG.md
Normal file
14
CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
The `[Unreleased]` section is automatically updated on every push to `main`.
|
||||
See [GitHub Releases](https://github.com/meshtastic/Meshtastic-Android/releases) for the full history.
|
||||
|
||||
<!-- UNRELEASED_START -->
|
||||
## [Unreleased]
|
||||
|
||||
*No changes yet.*
|
||||
<!-- UNRELEASED_END -->
|
||||
|
||||
<!-- RELEASED_START -->
|
||||
<!-- RELEASED_END -->
|
||||
Reference in New Issue
Block a user