diff --git a/.github/workflows/merge-pull-requests.yml b/.github/workflows/merge-pull-requests.yml index 6643e9f..8a3305b 100644 --- a/.github/workflows/merge-pull-requests.yml +++ b/.github/workflows/merge-pull-requests.yml @@ -1,6 +1,8 @@ name: Merge pull requests on: + schedule: + - cron: "30 5 * * 6" workflow_dispatch: inputs: repository: @@ -54,10 +56,11 @@ on: concurrency: group: "merge-pull-requests" - cancel-in-progress: true + cancel-in-progress: false jobs: merge: + if: github.event_name != 'schedule' || vars.AUTO_MERGE_RELEASES == 'true' runs-on: ubuntu-latest env: GH_TOKEN: ${{ secrets.PERSONAL_TOKEN }} @@ -68,7 +71,30 @@ jobs: with: egress-policy: audit + - name: Check release Saturday + id: cadence + env: + EVENT_NAME: ${{ github.event_name }} + run: | + if [[ "$EVENT_NAME" != "schedule" ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + days=$(( ($(date -u +%s) - $(date -u -d 2026-09-26 +%s)) / 86400 )) + if (( days % 14 == 0 )); then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "Not a release Saturday; skipping." >> "$GITHUB_STEP_SUMMARY" + fi + fi + - name: Merge pull requests + if: steps.cadence.outputs.run == 'true' + env: + SELECTED_REPO: ${{ inputs.repository || 'all' }} + BRANCH: ${{ inputs.branch || 'fossifybot/release' }} + MERGE_METHOD: ${{ inputs.merge_method || 'squash' }} + FORCE_ADMIN: ${{ inputs.force_admin || false }} + PR_AUTHOR: ${{ github.event_name == 'schedule' && 'fossifybot[bot]' || inputs.pr_author }} run: | set -uo pipefail @@ -92,12 +118,6 @@ jobs: "FossifyOrg/Voice-Recorder" ) - SELECTED_REPO="${{ github.event.inputs.repository }}" - BRANCH="${{ github.event.inputs.branch }}" - MERGE_METHOD="${{ github.event.inputs.merge_method }}" - FORCE_ADMIN="${{ github.event.inputs.force_admin }}" - PR_AUTHOR="${{ github.event.inputs.pr_author }}" - case "$MERGE_METHOD" in squash) MERGE_FLAG="--squash" ;; merge) MERGE_FLAG="--merge" ;; @@ -129,7 +149,7 @@ jobs: for repo in "${REPOS[@]}"; do echo "Processing: $repo" - PR_NUMBER=$(gh pr list --repo "$repo" --head "$BRANCH" "${SEARCH_ARGS[@]}" --json number --jq '.[0].number // empty' 2>/dev/null) + PR_NUMBER=$(gh pr list --repo "$repo" --head "$BRANCH" "${SEARCH_ARGS[@]}" --json number --jq '.[0].number // empty') if [[ -z "$PR_NUMBER" ]]; then echo "[$repo]: No open PR found for branch $BRANCH (author filter: '${PR_AUTHOR:-any}'). Skipping." continue @@ -137,16 +157,16 @@ jobs: echo "[$repo]: Found PR #$PR_NUMBER for branch $BRANCH" - if gh pr review --repo "$repo" --approve "$PR_NUMBER" 2>/dev/null; then + if gh pr review --repo "$repo" --approve "$PR_NUMBER"; then echo "[$repo]: PR #$PR_NUMBER approved" else echo "[$repo]: Failed to approve PR #$PR_NUMBER" fi - if gh pr merge --repo "$repo" $MERGE_FLAG $ADMIN_FLAG "$PR_NUMBER" 2>/dev/null; then - echo "[$repo]: PR #$PR_NUMBER merged successfully" + if gh pr merge --repo "$repo" "$MERGE_FLAG" "$ADMIN_FLAG" "$PR_NUMBER"; then + echo "[$repo]: PR #$PR_NUMBER merge submitted" | tee -a "$GITHUB_STEP_SUMMARY" else - echo "[$repo]: Failed to merge PR #$PR_NUMBER. Skipping." + echo "[$repo]: PR #$PR_NUMBER could not be merged. Skipping." | tee -a "$GITHUB_STEP_SUMMARY" fi sleep 1 diff --git a/.github/workflows/prepare-release-pr.yml b/.github/workflows/prepare-release-pr.yml index 71ef514..274e94d 100644 --- a/.github/workflows/prepare-release-pr.yml +++ b/.github/workflows/prepare-release-pr.yml @@ -45,18 +45,74 @@ jobs: echo "hasTags=false" >> "$GITHUB_OUTPUT" fi + - name: Setup NodeJS + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: "20" + + - name: Install keep-a-changelog parser + run: npm install keep-a-changelog@2.6.2 + + - name: Check unreleased changes + id: changelog + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + env: + HAS_TAGS: ${{ steps.release-history.outputs.hasTags }} + with: + script: | + const fs = require('fs'); + const { execFileSync } = require('child_process'); + const { parser } = require('keep-a-changelog'); + const parsed = parser(fs.readFileSync('CHANGELOG.md', 'utf8')); + const unreleased = parsed.findRelease(); + if (!unreleased) { + throw new Error('CHANGELOG.md has no Unreleased section.'); + } + + const version = fs.readFileSync('gradle.properties', 'utf8') + .match(/^VERSION_NAME=(.+)$/m)?.[1].trim(); + if (!version) { + throw new Error('VERSION_NAME is missing from gradle.properties.'); + } + core.setOutput('current_version', version); + + if (process.env.HAS_TAGS === 'true') { + const files = execFileSync('git', [ + 'diff', '--name-only', '--no-renames', `refs/tags/${version}`, 'HEAD', '--', + 'app/src/*/res/values-*/strings.xml', + 'fastlane/metadata/android/*/title.txt', + 'fastlane/metadata/android/*/short_description.txt', + 'fastlane/metadata/android/*/full_description.txt', + ':(exclude)fastlane/metadata/android/en-US/*', + ], { encoding: 'utf8' }).trim(); + const entries = unreleased.changes.get('changed') || []; + if (files && !entries.some(entry => entry.toString().includes('Updated translations'))) { + unreleased.changed('Updated translations'); + fs.writeFileSync('CHANGELOG.md', parsed.toString()); + core.info('Added Updated translations to Unreleased.'); + } + } + + const hasChanges = [...unreleased.changes.values()].some(entries => entries.length > 0); + core.setOutput('has_changes', hasChanges); + if (!hasChanges) { + core.info('No unreleased changelog entries or translation changes; skipping release preparation.'); + } + - name: Calculate next version name - if: steps.release-history.outputs.hasTags == 'true' + if: steps.release-history.outputs.hasTags == 'true' && steps.changelog.outputs.has_changes == 'true' id: semantic-version uses: ietf-tools/semver-action@c90370b2958652d71c06a3484129a4d423a6d8a8 with: token: ${{ github.token }} branch: ${{ github.event.repository.default_branch }} + fromTag: ${{ steps.changelog.outputs.current_version }} noNewCommitBehavior: "warn" - noVersionBumpBehavior: "warn" + noVersionBumpBehavior: "patch" patchList: fix, bugfix, perf, refactor, test, tests, chore - name: Get next version name + if: steps.changelog.outputs.has_changes == 'true' id: version-name env: HAS_TAGS: ${{ steps.release-history.outputs.hasTags }} @@ -96,69 +152,14 @@ jobs: echo "Release branch doesn't exist yet." fi - - name: Update files and check changelog + - name: Update version files if: ${{ steps.skip-update.outputs.needs_update == 'true' && steps.version-name.outputs.nextStrict }} id: update-version run: | - chmod +x .github-repo/scripts/update-version.sh - if ! .github-repo/scripts/update-version.sh \ + bash .github-repo/scripts/update-version.sh \ "${{ steps.version-name.outputs.nextStrict }}" \ "${{ steps.version-code.outputs.nextVersionCode }}" \ - "${{ github.repository }}"; then - echo "::error::Release aborted - No changes found in CHANGELOG.md [Unreleased] section" - echo "::error::Please add your changes to the [Unreleased] section before creating a release" - exit 1 - fi - - - name: Setup NodeJS - if: ${{ steps.update-version.outcome == 'success' }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: "20" - - - name: Install keep-a-changelog parser - if: ${{ steps.update-version.outcome == 'success' }} - run: npm install keep-a-changelog@2.6.2 - - - name: Add translations entry in changelog - if: ${{ steps.update-version.outcome == 'success' }} - id: update_changelog - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - script: | - const fs = require('fs'); - const path = require('path'); - const { parser } = require('keep-a-changelog'); - const changelogPath = path.join(process.env.GITHUB_WORKSPACE, 'CHANGELOG.md'); - - if (!fs.existsSync(changelogPath)) { - throw new Error(`Changelog file not found at ${changelogPath}`); - } - - const changelogContent = fs.readFileSync(changelogPath, 'utf8'); - let parsed; - try { - parsed = parser(changelogContent); - console.log(`Parsed ${parsed.releases.length} releases from changelog.`); - } catch (error) { - throw new Error(`Failed to parse CHANGELOG.md: ${error.message}`); - } - - const tagVersion = "${{ steps.version-name.outputs.nextStrict }}"; - const release = parsed.findRelease(tagVersion); - if (!release) { - throw new Error(`No changelog entry found for version ${tagVersion} in ${changelogPath}.`); - } - - const changedEntries = release.changes.get('changed') || []; - if (!changedEntries.some(entry => entry.toString().includes('Updated translations'))) { - release.changed('Updated translations'); - const updatedChangelog = parsed.toString(); - fs.writeFileSync(changelogPath, updatedChangelog, 'utf8'); - console.log(`Added "Updated translations" to version ${tagVersion}`); - } else { - console.log(`"Updated translations" already present for version ${tagVersion}.`); - } + "${{ github.repository }}" - name: Normalize changelog if: ${{ steps.update-version.outcome == 'success' }}