diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9395e79 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +--- +name: CI + +on: + pull_request: + branches: [master] + types: [opened, synchronize, reopened] + push: + branches: [master] + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + action: + environment: + ${{ github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository && + 'external' || 'internal' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Release + id: setup-release + uses: LizardByte/setup-release-action@fix!-updates # todo: change to latest + with: + github_token: ${{ secrets.GITHUB_TOKEN }} # can use GITHUB_TOKEN for read-only access + + - name: Set action variables + id: vars + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + changelog_branch=pr-changelogs + changelog_file=PR-${{ github.event.pull_request.number }}-CHANGELOG.md + else + changelog_branch=changelog + changelog_file=CHANGELOG.md + fi + + # set outputs + echo "changelog_branch=$changelog_branch" >> $GITHUB_OUTPUT + echo "changelog_file=$changelog_file" >> $GITHUB_OUTPUT + + - name: Run Action + id: action + uses: ./ + with: + changelogBranch: ${{ steps.vars.outputs.changelog_branch }} + changelogFile: ${{ steps.vars.outputs.changelog_file }} + token: ${{ secrets.GH_BOT_TOKEN }} + + - name: Create/Update GitHub Release + if: ${{ steps.setup-release.outputs.publish_release == 'true' }} + uses: LizardByte/create-release-action@fix!-updates # todo: change to latest + with: + allowUpdates: false + artifacts: "" + discussionCategory: announcements + generateReleaseNotes: true + name: ${{ steps.setup-release.outputs.release_tag }} + prerelease: true + tag: ${{ steps.setup-release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..225ce7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# ignore jetbrains files +.idea/ diff --git a/README.md b/README.md index fd3e658..a3cc366 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ -# template-base -Base repository template for LizardByte. +# update-changelog-action +[![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/update-changelog-action/ci.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/update-changelog-action/actions/workflows/ci.yml?query=branch%3Amaster) + +A reusable action to update a changelog, based on the contents of the GitHub releases. + +## Basic Usage + +See [action.yml](action.yml) + +```yaml +steps: + - name: Update Changelog + uses: LizardByte/update-changelog-action@master + with: + token: ${{ secrets.GITHUB_TOKEN }} +``` + +## Inputs + +| Name | Description | Default | Required | +|------------------------|----------------------------------------|-----------------|----------| +| changelogBranch | The branch to store the changelog in. | `changelog` | `false` | +| changelogFile | The file to store the changelog in. | `CHANGELOG.md` | `false` | +| token | GitHub Token. | | `true` | + +## Outputs + +| Name | Description | +|-----------|------------------------------------------| +| changelog | The contents of the generated changelog. | + +## See Also + +This action is meant to be used in conjunction with +[LizardByte/setup-release-action](https://github.com/LizardByte/setup-release-action). diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..e144025 --- /dev/null +++ b/action.yml @@ -0,0 +1,163 @@ +--- +name: "Update changelog" +description: "A reusable action to automatically update a changelog based on GitHub releases." +author: "LizardByte" +inputs: + 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' + token: + description: 'Github Token.' + required: true + +runs: + using: "composite" + steps: + - name: Create Changelog + env: + changelog_branch: ${{ inputs.changelogBranch }} + changelog_file: ${{ inputs.changelogFile }} + uses: actions/github-script@v7 + 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 = '\n\n' // commenting out due to how sphinx renders this + 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}` + + // replace lines such as `## Any Text\n` with `**Any Text**\n` + // let release_body = release.body.replace(/## (.*)\n/g, '**$1**\n') + // replace lines such as `## Any Text\n` with `### Any Text\n` + let release_body = release.body.replace(/## (.*)\n/g, '### $1\n') + changelog += `## [${release.tag_name}] - ${date}\n\n${release_body}\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);