From c09f7fd5e670e8d0fe536d8e164f024ffdd437dd Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Mon, 23 Mar 2026 17:45:43 +0000 Subject: [PATCH] devex: changelog system (#5309) * devex: changelog system * feat: changelog CIs * feat: web alias for platform + hosting * feat: upload binaries to gh release * feat: improve copy text * fix: release workflow * fix: changelog CIs + PR health check comment * fix: action * fix: comment style * fix: comment * fix: remove health * fix: deploy use Modrinth bot machine account * feat: new system * fix: pr comment structure --- .github/workflows/changelog-comment.yml | 121 +++++ .github/workflows/frontend-preview.yml | 3 +- .github/workflows/theseus-release.yml | 44 +- apps/frontend/nuxt.config.ts | 2 +- .../pages/news/changelog/[product]/[date].vue | 2 +- .../src/pages/news/changelog/index.vue | 12 +- package.json | 6 +- packages/{utils => blog}/changelog.ts | 2 +- packages/blog/index.ts | 1 + packages/blog/package.json | 1 + packages/ui/package.json | 1 + .../components/changelog/ChangelogEntry.vue | 8 +- packages/ui/src/locales/en-US/index.json | 5 +- packages/utils/index.ts | 1 - pnpm-lock.yaml | 226 +++++----- scripts/collect-changelog.ts | 425 ++++++++++++++++++ 16 files changed, 696 insertions(+), 164 deletions(-) create mode 100644 .github/workflows/changelog-comment.yml rename packages/{utils => blog}/changelog.ts (99%) create mode 100644 scripts/collect-changelog.ts diff --git a/.github/workflows/changelog-comment.yml b/.github/workflows/changelog-comment.yml new file mode 100644 index 000000000..a96667c92 --- /dev/null +++ b/.github/workflows/changelog-comment.yml @@ -0,0 +1,121 @@ +name: Changelog Comment + +on: + pull_request: + types: [opened, reopened] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to post the changelog comment on (for testing)' + required: true + type: number + +jobs: + comment: + name: Post changelog comment + runs-on: ubuntu-latest + + steps: + - name: 💬 Post or update changelog comment + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.CROWDIN_GH_TOKEN }} + script: | + const marker = ''; + const mergedMarker = ''; + + const sections = ['### Added', '', '### Changed', '', '### Deprecated', '', '### Removed', '', '### Fixed', '', '### Security'].join('\n'); + const productBlock = (name) => `
\n${name}\n\n${sections}\n\n
`; + + const template = [ + marker, + '## Pull request changelog', + '', + '', + '', + productBlock('App'), + '', + productBlock('Website'), + '', + productBlock('Hosting'), + ].join('\n'); + + // Resolve PR number from event or workflow_dispatch input + const prNumber = context.payload.pull_request?.number + ?? parseInt('${{ github.event.inputs.pr_number }}', 10); + + if (!prNumber || isNaN(prNumber)) { + core.setFailed('Could not determine PR number'); + return; + } + + // Get PR details (need base ref for child PR detection) + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + // Check if bot comment already exists + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const existingComment = comments.find(c => c.body.includes(marker)); + if (existingComment) { + core.info('Changelog comment already exists, skipping'); + return; + } + + // Post the template comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: template, + }); + + core.info(`Posted changelog comment on PR #${prNumber}`); + + // Detect child PR: check if this PR's base branch is another open PR's head branch + const baseRef = pr.base.ref; + + if (baseRef === 'main' || baseRef === 'prod') { + return; + } + + // Look for a parent PR whose head branch matches our base branch + const { data: candidatePRs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:${baseRef}`, + }); + + if (candidatePRs.length === 0) { + return; + } + + const parentPR = candidatePRs[0]; + core.info(`Detected parent PR #${parentPR.number} for child PR #${prNumber}`); + + // Add admonition to child PR's changelog comment + const { data: childComments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const childChangelogComment = childComments.find(c => c.body.includes(marker)); + if (childChangelogComment && !childChangelogComment.body.includes(mergedMarker)) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: childChangelogComment.id, + body: `${mergedMarker}\n> [!NOTE]\n> This changelog has been merged into the changelog for #${parentPR.number}\n\n${childChangelogComment.body}`, + }); + } diff --git a/.github/workflows/frontend-preview.yml b/.github/workflows/frontend-preview.yml index e75a7122b..eecd219f3 100644 --- a/.github/workflows/frontend-preview.yml +++ b/.github/workflows/frontend-preview.yml @@ -50,14 +50,15 @@ jobs: uses: peter-evans/find-comment@v3 id: fc with: + token: ${{ secrets.CROWDIN_GH_TOKEN }} issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' body-includes: Frontend previews - name: Comment deploy URL on PR if: github.event_name == 'pull_request' uses: peter-evans/create-or-update-comment@v5 with: + token: ${{ secrets.CROWDIN_GH_TOKEN }} issue-number: ${{ github.event.pull_request.number }} comment-id: ${{ steps.fc.outputs.comment-id }} body: | diff --git a/.github/workflows/theseus-release.yml b/.github/workflows/theseus-release.yml index 6cd2be01f..e3319f259 100644 --- a/.github/workflows/theseus-release.yml +++ b/.github/workflows/theseus-release.yml @@ -1,47 +1,43 @@ name: Modrinth App release on: - workflow_dispatch: - inputs: - version-tag: - description: Version tag to release to the wide public - type: string - required: true - release-notes: - description: Release notes to include in the Tauri version manifest - default: A new release of the Modrinth App is available! - type: string - required: true + workflow_run: + workflows: ['Modrinth App build'] + types: [completed] jobs: release: name: Release Modrinth App + if: >- + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'v') runs-on: ubuntu-latest env: + VERSION_TAG: ${{ github.event.workflow_run.head_branch }} LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu) WINDOWS_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-pc-windows-msvc) MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME: App bundle (universal-apple-darwin) LAUNCHER_FILES_BUCKET_BASE_URL: https://launcher-files.modrinth.com steps: + - name: 📥 Check out code + uses: actions/checkout@v4 + - name: 📥 Download Modrinth App artifacts uses: dawidd6/action-download-artifact@v11 with: workflow: theseus-build.yml workflow_conclusion: success event: push - branch: ${{ inputs.version-tag }} + branch: ${{ env.VERSION_TAG }} use_unzip: true - name: 🛠️ Generate version manifest - env: - VERSION_TAG: ${{ inputs.version-tag }} - RELEASE_NOTES: ${{ inputs.release-notes }} run: | # Reference: https://tauri.app/plugin/updater/#server-support jq -nc \ --arg versionTag "${VERSION_TAG#v}" \ - --arg releaseNotes "$RELEASE_NOTES" \ + --arg releaseNotes "See the full changelog at https://modrinth.com/news/changelog" \ --rawfile macOsAarch64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \ --rawfile macOsX64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \ --rawfile linuxX64UpdateArtifactSignature "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/appimage/Modrinth App_${VERSION_TAG#v}_amd64.AppImage.tar.gz.sig" \ @@ -83,7 +79,6 @@ jobs: - name: 📤 Upload release artifacts env: - VERSION_TAG: ${{ inputs.version-tag }} AWS_ACCESS_KEY_ID: ${{ secrets.LAUNCHER_FILES_BUCKET_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.LAUNCHER_FILES_BUCKET_SECRET_ACCESS_KEY }} AWS_BUCKET: ${{ secrets.LAUNCHER_FILES_BUCKET_NAME }} @@ -116,3 +111,18 @@ jobs: done aws s3 cp updates.json "s3://${AWS_BUCKET}" + + - name: 🏷️ Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${VERSION_TAG#v}" + + gh release create "$VERSION_TAG" \ + --title "Modrinth App ${VERSION}" \ + --notes "See the full changelog at https://modrinth.com/news/changelog" \ + "${WINDOWS_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/nsis/Modrinth App_${VERSION}_x64-setup.exe" \ + "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/dmg/Modrinth App_${VERSION}_universal.dmg" \ + "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/appimage/Modrinth App_${VERSION}_amd64.AppImage" \ + "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/deb/Modrinth App_${VERSION}_amd64.deb" \ + "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/rpm/Modrinth App-${VERSION}-1.x86_64.rpm" diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts index 56ed1b5cc..693693853 100644 --- a/apps/frontend/nuxt.config.ts +++ b/apps/frontend/nuxt.config.ts @@ -115,7 +115,7 @@ export default defineNuxtConfig({ await import('./src/templates/docs/index.ts').then((m) => m.default), ) const blogArticles = await import('@modrinth/blog').then((m) => m.articles) - const { getChangelog } = await import('@modrinth/utils') + const { getChangelog } = await import('@modrinth/blog') nitroConfig.prerender = nitroConfig.prerender || {} nitroConfig.prerender.routes = nitroConfig.prerender.routes || [] diff --git a/apps/frontend/src/pages/news/changelog/[product]/[date].vue b/apps/frontend/src/pages/news/changelog/[product]/[date].vue index 931419d68..7005fe0ab 100644 --- a/apps/frontend/src/pages/news/changelog/[product]/[date].vue +++ b/apps/frontend/src/pages/news/changelog/[product]/[date].vue @@ -1,7 +1,7 @@