diff --git a/.github/ISSUE_TEMPLATE/minor-release.md b/.github/ISSUE_TEMPLATE/minor-release.md index e533e1b5fcb6c..a9d49abfafdfb 100644 --- a/.github/ISSUE_TEMPLATE/minor-release.md +++ b/.github/ISSUE_TEMPLATE/minor-release.md @@ -16,15 +16,10 @@ The week before the release: - [ ] Check if there is a newer version of Alpine or Debian available to update the release images in `distribution/docker/`. Update if so. - [ ] Run `cargo vdev build release-cue` to generate a new cue file for the release -- [ ] Add `changelog` key to generated cue file - - [ ] `git log --no-merges --cherry-pick --right-only ...` - - [ ] Should be hand-written list of changes - ([example](https://github.com/vectordotdev/vector/blob/9fecdc8b5c45c613de2d01d4d2aee22be3a2e570/website/cue/reference/releases/0.19.0.cue#L44)) - - [ ] Ensure any breaking changes are highlighted in the release upgrade guide - - [ ] Ensure any deprecations are highlighted in the release upgrade guide - - [ ] Ensure all notable features have a highlight written - [ ] Add description key to the generated cue file with a description of the release (see previous releases for examples). + - [ ] Ensure any breaking changes are highlighted in the release upgrade guide + - [ ] Ensure any deprecations are highlighted in the release upgrade guide - [ ] Update version number in `website/cue/reference/administration/interfaces/kubectl.cue` - [ ] Update version number in `distribution/install.sh` - [ ] Add new version to `website/cue/reference/versions.cue` diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml new file mode 100644 index 0000000000000..702041efaf930 --- /dev/null +++ b/.github/workflows/changelog.yaml @@ -0,0 +1,51 @@ +# Changelog +# +# Validates that a changelog entry was added. +# Runs on PRs when: +# - openen/re-opened +# - new commits pushed +# - label is added or removed + +name: Changelog + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + +jobs: + check-changelog: + env: + PR_HAS_LABEL: ${{ contains( github.event.pull_request.labels.*.name, 'no-changelog') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Get PR comment author + id: author + uses: tspascoal/get-user-teams-membership@v3 + with: + username: ${{ github.actor }} + team: 'Vector' + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + + - + env: + EXTERN_CONTRIBUTOR: ${{ steps.author.outputs.isTeamMember }} + run: | + if [[ $PR_HAS_LABEL == 'true' ]] ; then + echo "'no-changelog' label detected." + exit 0 + fi + + # helper script needs to reference the main branch to compare against + git fetch origin main:refs/remotes/origin/master + + # If the PR author is an external contributor, validate that the + # changelog fragments added contain the author line for website rendering. + args="" + if [[ $PR_HAS_LABEL == 'true' ]] ; then + echo "PR author detected to be an external contributor." + args="--author" + fi + + ./scripts/check_changelog_fragments.sh ${args} diff --git a/changelog.d/README.md b/changelog.d/README.md new file mode 100644 index 0000000000000..bfe819e88d8b3 --- /dev/null +++ b/changelog.d/README.md @@ -0,0 +1,70 @@ +## Overview + +This directory contains changelog "fragments" that are collected during a release to +generate the project's user facing changelog. + +The tool used to generate the changelog is [towncrier](https://towncrier.readthedocs.io/en/stable/markdown.html). + +The configuration file is `changelog.toml`. +The changelog fragments are are located in `changelog.d/`. + +## Process + +Fragments for un-released changes are placed in the root of this directory during PRs. + +During a release when the changelog is generated, the fragments in the root of this +directory are moved into a new directory with the name of the release (e.g. '0.42.0'). + +### Pull Requests + +By default, PRs are required to add at least one entry to this directory. +This is enforced during CI. + +To mark a PR as not requiring user-facing changelog notes, add the label 'no-changelog'. + +To run the same check that is run in CI to validate that your changelog fragments have +the correct syntax, commit the fragment additions and then run ./scripts/check_changelog_fragments.sh + +The format for fragments is: \.\.md + +### Fragment conventions + +When fragments used to generate the updated changelog, the content of the fragment file is +rendered as an item in a bulleted list under the "type" of fragment. + +The contents of the file must be valid markdown. + +Filename rules: +- Must begin with the PR number associated with the change. +- The type must be one of: breaking|security|deprecated|feature|enhanced|fixed. + These types are described in more detail in the config file (see `changelog.toml`). +- Only the two period delimiters can be used. +- The file must be markdown. + +### Breaking changes + +When using the type 'breaking' to add notes for a breaking change, these should be more verbose than +other entries typically. It should include all details that would be relevant for the user to need +to handle upgrading to the breaking change. + +## Example + +Here is an example of a changelog fragment that adds a breaking change explanation. + + $ cat changelog.d/42.breaking.md + This change is so great. It's such a great change that this sentence + explaining the change has to span multiple lines of text. + + It even necessitates a line break. It is a breaking change after all. + +This renders in the auto generated changelog as: +(note that PR links are omitted in the public facing version of the changelog) + + ## [X.X.X] + + ### Breaking Changes & Upgrade Guide + + - This change is so great. It's such a great change that this sentence + explaining the change has to span multiple lines of text. + + It even necessitates a line break. It is a breaking change after all. ([#42])(https://github.com/vectordotdev/vector/pull/42)) diff --git a/changelog.toml b/changelog.toml new file mode 100644 index 0000000000000..bc26798ebae23 --- /dev/null +++ b/changelog.toml @@ -0,0 +1,57 @@ +# Configuration for `towncrier` used during release process to auto-generate changelog. + +[tool.towncrier] +directory = "changelog.d" +filename = "CHANGELOG.md" +start_string = "\n" +underlines = ["", "", ""] +title_format = "## [{version}]" +issue_format = "[#{issue}](https://github.com/vectordotdev/vector/pull/{issue})" + +# The following configurations specify which fragment "types" are +# allowed. +# +# If a change applies to more than one type, select the one it most +# applies to. Or, if applicable, multiple changelog fragments can be +# added for one PR. For example, if a PR includes a breaking change +# around some feature, but also fixes a bug in the same part of the +# code but is tangential to the breaking change, a separate +# fragment can be added to call out the fix. + +# A change that is incompatible with prior versions which +# requires users to make adjustments. +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking Changes & Upgrade Guide" +showcontent = true + +# A change that has implications for security. +[[tool.towncrier.type]] +directory = "security" +name = "Security" +showcontent = true + +# A change that is introducing a deprecation. +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecations" +showcontent = true + +# A change that is introducing a new feature. +[[tool.towncrier.type]] +directory = "feature" +name = "New Features" +showcontent = true + +# A change that is enhancing existing functionality in a user +# perceivable way. +[[tool.towncrier.type]] +directory = "enhanced" +name = "Enhancements" +showcontent = true + +# A change that is fixing a bug. +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixes" +showcontent = true diff --git a/docs/DEPRECATION.md b/docs/DEPRECATION.md index 4d8c82faaa256..0009fd0bb86c0 100644 --- a/docs/DEPRECATION.md +++ b/docs/DEPRECATION.md @@ -70,6 +70,8 @@ When introducing a deprecation into Vector, the pull request introducing the dep - Add a note to the Deprecations section of the upgrade guide for the next release with a description and directions for transitioning if applicable. +- Copy the same note from the previous step, to a changelog fragment, with type="breaking". See the changelog + fragment [README.md](../changelog.d/README.md) for details. - Add a deprecation note to the docs. Typically, this means adding `deprecation: "description of the deprecation"` to the `cue` data for the option or feature. If the `cue` schema does not support `deprecation` for whatever you are deprecating yet, add it to the schema and open an issue to have it rendered on the website. @@ -86,4 +88,6 @@ When removing a deprecation in a subsequent release, the pull request should: - Remove the deprecation from the documentation - Add a note to the Breaking Changes section of the upgrade guide for the next release with a description and directions for transitioning if applicable. +- Copy the same note from the previous step, to a changelog fragment, with type="breaking". See the changelog + fragment [README.md](../changelog.d/README.md) for details. - Remove the deprecation from [DEPRECATIONS.md](DEPRECATIONS.md) diff --git a/scripts/check_changelog_fragments.sh b/scripts/check_changelog_fragments.sh new file mode 100755 index 0000000000000..958b3f5f83703 --- /dev/null +++ b/scripts/check_changelog_fragments.sh @@ -0,0 +1,75 @@ +#/bin/bash + +# This script is intended to run during CI, however it can be run locally by +# committing changelog fragments before executing the script. If the script +# finds an issue with your changelog fragment, you can unstage the fragment +# from being committed and fix the issue. + +OPW_CHANGELOG_DIR="changelog.d" +FRAGMENT_TYPES="breaking|security|deprecated|feature|enhanced|fixed" + +if [ ! -d "${OPW_CHANGELOG_DIR}" ]; then + echo "No ./${OPW_CHANGELOG_DIR} found. This tool must be invoked from the root of the OPW repo." + exit 1 +fi + +# diff-filter=A lists only added files +ADDED=$(git diff --name-only --diff-filter=A origin/master ${OPW_CHANGELOG_DIR}) + +if [ $(echo "$ADDED" | grep -c .) -lt 1 ]; then + echo "No changelog fragments detected" + echo "If no changes necessitate user-facing explanations, add the GH label 'no-changelog'" + echo "Otherwise, add changelog fragments to changelog.d/" + exit 1 +fi + +# extract the basename from the file path +ADDED=$(echo ${ADDED} | xargs -n1 basename) + +# validate the fragments +while IFS= read -r fname; do + + if [[ ${fname} == "README.md" ]]; then + continue + fi + + echo "validating '${fname}'" + + arr=(${fname//./ }) + + if [ "${#arr[@]}" -ne 3 ]; then + echo "invalid fragment filename: wrong number of period delimiters. expected '..md'. (${fname})" + exit 1 + fi + + if ! [[ "${arr[0]}" =~ ^[0-9]+$ ]]; then + echo "invalid fragment filename: fragment must begin with an integer (PR number). expected '..md' (${fname})" + exit 1 + fi + + if ! [[ "${arr[1]}" =~ ^(${FRAGMENT_TYPES})$ ]]; then + echo "invalid fragment filename: fragment type must be one of: (${FRAGMENT_TYPES}). (${fname})" + exit 1 + fi + + if [[ "${arr[2]}" != "md" ]]; then + echo "invalid fragment filename: extension must be markdown (.md): (${fname})" + exit 1 + fi + + # if specified, this option validates that the contents of the news fragment + # contains a properly formatted authors line at the end of the file, generally + # used for external contributor PRs. + if [[ $1 == "--authors" ]]; then + last=$( tail -n 1 "${OPW_CHANGELOG_DIR}/${fname}" ) + if ! [[ "${last}" =~ ^(authors: .*)$ ]]; then + echo "invalid fragment contents: author option was specified but fragment ${fname} contains no authors." + exit 1 + fi + + fi + +done <<< "$ADDED" + + +echo "changelog additions are valid." diff --git a/scripts/generate-release-cue.rb b/scripts/generate-release-cue.rb index 7191c3594a7e8..c545ae4229534 100755 --- a/scripts/generate-release-cue.rb +++ b/scripts/generate-release-cue.rb @@ -26,6 +26,7 @@ ROOT = ".." RELEASE_REFERENCE_DIR = File.join(ROOT, "website", "cue", "reference", "releases") +CHANGELOG_DIR = File.join(ROOT, "changelog.d") TYPES = ["chore", "docs", "feat", "fix", "enhancement", "perf"] TYPES_THAT_REQUIRE_SCOPES = ["feat", "enhancement", "fix"] @@ -109,6 +110,87 @@ def create_log_file!(current_commits, new_version) release_log_path end +def generate_changelog!(commits) + + entries = "" + + Dir.glob("#{CHANGELOG_DIR}/*.md") do |fname| + + # Util::Printer.title("#{fname}") + if File.basename(fname) == "README.md" + next + end + + if entries != "" + entries += ",\n" + end + + # description = File.read(fname) + fragment_contents = File.open(fname) + + # add the GitHub username for any fragments + # that have an authors field at the end of the + # fragment. This is generally used for external + # contributor PRs. + lines = fragment_contents.to_a + last = lines.last + contributors = Array.new + + if last.start_with?("authors: ") + authors_str = last[9..] + authors_str = authors_str.delete(" \t\r\n") + authors_arr = authors_str.split(",") + authors_arr.each { |author| contributors.push(author) } + + # remove that line from the description + lines.pop() + end + + description = lines.join("") + + # get the PR number of the changelog fragment. + # the fragment type is not used in the Vector release currently. + basename = File.basename(fname, "*.md") + parts = basename.split(".") + + pr_number = parts[0].to_i + pr_numbers = Array(pr_number) + + commit = commits.find { |commit| + commit.pr_number == pr_number + } + + if commit.nil? + Util::Printer.error!("Changelog fragment #{fname} PR number does not match any commit.") + end + + scopes = commit.scopes + type = commit.type + + entry = "{\n" + + "type: #{type.to_json}\n" + + "scopes: #{scopes.to_json}\n" + + "description: \"\"\"\n" + + "#{description}" + + "\"\"\"\n" + + "pr_numbers: #{pr_numbers.to_json}\n" + + if commit.breaking_change == true + entry += "breaking: #{commit.breaking_change.to_json}\n" + end + + if contributors.length() > 0 + entry += "contributors: #{contributors.to_json}\n" + end + + entry += "}" + + entries += entry + end + + entries +end + def create_release_file!(new_version) release_log_path = "#{RELEASE_REFERENCE_DIR}/#{new_version}.log" git_log = Vector::GitLogCommit.from_file!(release_log_path) @@ -120,6 +202,8 @@ def create_release_file!(new_version) commits.each(&:validate!) cue_commits = commits.collect(&:to_cue_struct).join(",\n ") + changelog_entries = generate_changelog!(commits) + if File.exists?(release_reference_path) words = <<~EOF @@ -150,6 +234,10 @@ def create_release_file!(new_version) whats_next: [] + changelog: [ + #{changelog_entries} + ] + commits: [ #{cue_commits} ]