diff --git a/.github/workflows/main-push-changelog.yml b/.github/workflows/main-push-changelog.yml deleted file mode 100644 index da161e44e..000000000 --- a/.github/workflows/main-push-changelog.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/models_pr_triage.yml b/.github/workflows/models_pr_triage.yml index c2a1aaf25..c5b670b79 100644 --- a/.github/workflows/models_pr_triage.yml +++ b/.github/workflows/models_pr_triage.yml @@ -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; diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index fa68a597b..92ed400d2 100644 --- a/.github/workflows/pr_enforce_labels.yml +++ b/.github/workflows/pr_enforce_labels.yml @@ -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) { diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index df16866f3..9be7b6c0f 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -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 = '' + ue = '' + rs = '' + + 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: diff --git a/.github/workflows/pull-request-target.yml b/.github/workflows/pull-request-target.yml index d37cecf43..f441a6b83 100644 --- a/.github/workflows/pull-request-target.yml +++ b/.github/workflows/pull-request-target.yml @@ -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}`); } diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 000000000..db19809d7 --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -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 <
+## [Unreleased] + +*No changes yet.* + + + +