From 436b27f33b4ddf42fe6cce72a38e0b1f8666309c Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:11:16 -0400 Subject: [PATCH] feat: generate changelog file --- .github/workflows/ci.yml | 15 +++- README.md | 31 +++++--- action.yml | 155 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc64833..9078157 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,16 +36,22 @@ jobs: id: vars run: | if [ "${{ github.event_name }}" == "pull_request" ]; then + changelog_branch=pr-changelogs + changelog_file=PR-${{ github.event.pull_request.number }}-CHANGELOG.md discussion_category=tests publish_pre_release=true release_tag=pr-${{ github.event.pull_request.number }}-${{ github.run_id }} else + changelog_branch=changelog + changelog_file=CHANGELOG.md discussion_category=announcements publish_pre_release=${{ steps.setup-release.outputs.publish_pre_release }} release_tag=${{ steps.setup-release.outputs.release_tag }} fi # set outputs + echo "changelog_branch=$changelog_branch" >> $GITHUB_OUTPUT + echo "changelog_file=$changelog_file" >> $GITHUB_OUTPUT echo "discussion_category=$discussion_category" >> $GITHUB_OUTPUT echo "publish_stable_release=$publish_stable_release" >> $GITHUB_OUTPUT echo "release_tag=$release_tag" >> $GITHUB_OUTPUT @@ -57,6 +63,8 @@ jobs: allowUpdates: false artifacts: '' body: '' + changelogBranch: ${{ steps.vars.outputs.changelog_branch }} + changelogFile: ${{ steps.vars.outputs.changelog_file }} discussionCategory: ${{ steps.vars.outputs.discussion_category }} generateReleaseNotes: true name: ${{ steps.vars.outputs.release_tag }} @@ -64,14 +72,17 @@ jobs: tag: ${{ steps.vars.outputs.release_tag }} token: ${{ secrets.GH_BOT_TOKEN }} + - name: Print changelog + run: | + echo "${{ steps.action.outputs.changelog }}" + - name: Sleep if: ${{ always() && github.event_name == 'pull_request' }} run: sleep 60 - name: Delete Release - env: - GITHUB_TOKEN: ${{ secrets.GH_BOT_TOKEN }} if: ${{ always() && github.event_name == 'pull_request' }} uses: dev-drprasad/delete-tag-and-release@v1.0.1 with: + github_token: ${{ secrets.GH_BOT_TOKEN }} tag_name: ${{ steps.vars.outputs.release_tag }} diff --git a/README.md b/README.md index 02a3c4c..bb56860 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,26 @@ steps: ## Inputs -| Name | Description | Default | Required | -|----------------------|--------------------------------------------------------------------------------------|-----------------|----------| -| allowUpdates | An optional flag which indicates if we should update a release if it already exists. | `true` | `false` | -| artifacts | The artifacts to upload. | `*artifacts/*` | `false` | -| body | The body of the release. | | `false` | -| discussionCategory | The category for the discussion. | `announcements` | `false` | -| generateReleaseNotes | Indicates if release notes should be automatically generated. | `false` | `false` | -| name | The version to create. | | `true` | -| prerelease | Whether the release is a prerelease. | `false` | `false` | -| tag | The tag to create. | | `true` | -| token | GitHub Token. | | `true` | +| Name | Description | Default | Required | +|----------------------|---------------------------------------------------------------------------------------------------|-----------------|----------| +| allowUpdates | An optional flag which indicates if we should update a release if it already exists. | `true` | `false` | +| artifacts | The artifacts to upload. | `*artifacts/*` | `false` | +| body | The body of the release. | | `false` | +| changelog | Whether to create a changelog file. The changelog will be generated from the GitHub releases API. | `true` | `false` | +| changelogBranch | The branch to store the changelog in. | `changelog` | `false` | +| changelogFile | The file to store the changelog in. | `CHANGELOG.md` | `false` | +| discussionCategory | The category for the discussion. | `announcements` | `false` | +| generateReleaseNotes | Indicates if release notes should be automatically generated. | `false` | `false` | +| name | The version to create. | | `true` | +| prerelease | Whether the release is a prerelease. | `false` | `false` | +| tag | The tag to create. | | `true` | +| token | GitHub Token. | | `true` | + +## Outputs + +| Name | Description | +|-----------|------------------------------------------| +| changelog | The contents of the generated changelog. | ## See Also diff --git a/action.yml b/action.yml index 71d43b3..5a138c0 100644 --- a/action.yml +++ b/action.yml @@ -15,6 +15,18 @@ inputs: description: 'The body of the release.' required: false default: '' + changelog: + description: 'Whether to create a changelog file.' + required: false + default: 'true' + changelogBranch: + description: 'The branch to store the changelog in.' + required: false + default: 'changelog' + changelogFile: + description: 'The file to store the changelog in.' + required: false + default: 'CHANGELOG.md' discussionCategory: description: 'The category of the discussion.' required: false @@ -57,3 +69,146 @@ runs: prerelease: ${{ inputs.prerelease }} tag: ${{ inputs.tag }} token: ${{ inputs.token }} + + - name: Create Changelog + env: + changelog_branch: ${{ inputs.changelogBranch }} + changelog_file: ${{ inputs.changelogFile }} + if: >- + inputs.changelog == 'true' && + (github.repository == 'LizardByte/create-release-action' || + (github.event_name == 'push' && github.ref == 'refs/heads/master')) + uses: actions/github-script@v6 + with: + github-token: ${{ inputs.token }} + script: | + // get inputs + const changelogBranch = process.env.changelog_branch + const changelogFile = process.env.changelog_file + + // get all releases and sort by date created, page if required + let releases = [] + let page = 1 + let per_page = 100 + let total = 0 + do { + const response = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: per_page, + page: page + }) + releases = releases.concat(response.data) + total = response.data.length + page++ + } while (total == per_page) + // sort releases by date created + releases.sort((a, b) => { + return new Date(a.created_at) - new Date(b.created_at) + }) + + // create a CHANGELOG.md and initialize it + let changelog = '# Changelog\n\n' + changelog += 'All notable changes to this project will be documented in this file.\n\n' + changelog += 'The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\n' + changelog += 'and this project adheres to [Calendar Versioning](https://calver.org/).\n\n' + changelog += 'This changelog was automatically generated by the\n' + changelog += '[create-release-action](https://github.com/LizardByte/create-release-action).\n\n' + + // loop through each release + releases = releases.reverse() + for (const release of releases) { + // add release to changelog + let created_date = new Date(release.created_at) + let year = created_date.getFullYear() + let month = (created_date.getMonth() + 1).toString().padStart(2, '0') + let day = created_date.getDate().toString().padStart(2, '0') + let date = `${year}-${month}-${day}` + changelog += `## [${release.tag_name}] - ${date}\n\n${release.body.replace('## ', '### ')}\n\n` + } + + // add urls to end of changelog + for (const release of releases) { + // add release url to changelog + changelog += `[${release.tag_name}]: ${release.html_url}\n` + } + + try { + // Directly create a tree with CHANGELOG.md in it + const blob = await github.rest.git.createBlob({ + owner: context.repo.owner, + repo: context.repo.repo, + content: changelog, + encoding: 'utf-8' + }) + + const tree = await github.rest.git.createTree({ + owner: context.repo.owner, + repo: context.repo.repo, + tree: [{ + path: changelogFile, + mode: '100644', + type: 'blob', + sha: blob.data.sha + }] + }) + + const commit = await github.rest.git.createCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + message: `chore: create ${changelogFile}`, + tree: tree.data.sha, + parents: [] // Empty parents array for a truly orphaned commit + }) + + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/heads/${changelogBranch}`, + sha: commit.data.sha + }) + } catch (e) { + if (e.status === 422 && e.message.includes("Reference already exists")) { + let sha = null; // Initialize the sha to null by default + + // Branch already exists, try to fetch the SHA of the specified changelogFile + try { + const fileData = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: changelogFile, + ref: changelogBranch + }); + + sha = fileData.data.sha; // Update the sha if the file exists + } catch (getFileError) { + if (getFileError.status !== 404) { + // If the error is not a 'not found' error, set the action as failed + core.setFailed(`Failed to fetch the file: ${getFileError.message}`); + return; + } + // If the error is 'not found' error, we'll continue with sha as null, resulting in file creation + } + + try { + // Create or Update the file using the same call + await github.rest.repos.createOrUpdateFileContents({ + owner: context.repo.owner, + repo: context.repo.repo, + path: changelogFile, + message: sha ? `chore: update ${changelogFile}` : `chore: create ${changelogFile}`, + content: Buffer.from(changelog).toString('base64'), + sha: sha, // if sha is null, it'll create a new file + branch: changelogBranch + }); + } catch (updateError) { + core.setFailed(`Failed to create or update the file: ${updateError.message}`); + } + } else { + // Some other error occurred + core.setFailed(`Action failed with error: ${e.message}`); + } + } + + // Set GitHub action output + core.setOutput('changelog', changelog);