From f0edbcef903c2eaaf41afe6afc0a8cf76f8a2f77 Mon Sep 17 00:00:00 2001 From: Jamie Gaskins Date: Wed, 28 Jul 2021 10:10:33 -0400 Subject: [PATCH] Add support for versioned releases (#13750) * Add release scripts * Use `delete` over `gsub` with an empty string Co-authored-by: Michael Kohl * wip * wip * Include stable release-channel branches in builds * Add changelog stub * Include the union of changes to the changelog * Add things * Add stuff * Remove test changelog content * Fix String#delete call Extraneous `.` made Ruby parse it as a range * Fix suggested release command * Use backticks to denote a command to run * Add debugging output to script * Build more appropriate containers for stable This includes tags * Check if branch *starts with* release channel name * Cache tag builds from production tag * Skip trying to build containers for untagged pushes * Clear out stubbed changelog * Update PR template to incorporate CHANGELOG.md * Run Rubocop on release scripts This excludes some linter rules that only make sense in code that is running as part of the Rails app. For example, we can't rely on `ActiveSupport::TimeWithZone` because these scripts don't load Rails, and we define top-level methods because they're scripts rather than complex applications. Co-authored-by: Michael Kohl --- .gitattributes | 1 + .github/PULL_REQUEST_TEMPLATE.md | 2 + CHANGELOG.md | 3 ++ package.json | 3 ++ scripts/build_containers.sh | 60 +++++++++++++++++++++ scripts/release | 93 ++++++++++++++++++++++++++++++++ scripts/stage_release | 64 ++++++++++++++++++++++ 7 files changed, 226 insertions(+) create mode 100644 CHANGELOG.md create mode 100755 scripts/release create mode 100755 scripts/stage_release diff --git a/.gitattributes b/.gitattributes index 276cfa7ba..aaf24388c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,4 @@ *.css diff=css *.html diff=html *.erb diff=html +/CHANGELOG.md merge=union diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b0fb337a9..6cff4fb11 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -60,6 +60,8 @@ will share this change with the people who need to know about it._ [Admin Guide](https://admin.forem.com/), or [Storybook](https://storybook.forem.com/) (for Crayons components) - [ ] I've updated the README or added inline documentation +- [ ] I've added an entry to + [`CHANGELOG.md`](https://github.com/forem/forem/tree/main/CHANGELOG.md) - [ ] I will share this change in a [Changelog](https://forem.dev/t/changelog) or in a [forem.dev](http://forem.dev) post - [ ] I will share this change internally with the appropriate teams diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..0c4e965ec --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 2021-07-23 + +Initial versioned release diff --git a/package.json b/package.json index c0d4ad013..bb77cde48 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,9 @@ "{app,spec,config,lib}/**/*.rb": [ "bundle exec rubocop --require rubocop-rspec --auto-correct" ], + "scripts/{release,stage_release}": [ + "bundle exec rubocop --require rubocop-rspec --auto-correct --except Style/TopLevelMethodDefinition,Rails/Date" + ], "app/views/**/*.jbuilder": [ "bundle exec rubocop --require rubocop-rspec --auto-correct" ], diff --git a/scripts/build_containers.sh b/scripts/build_containers.sh index 614966c7d..eb4223d26 100755 --- a/scripts/build_containers.sh +++ b/scripts/build_containers.sh @@ -102,6 +102,56 @@ function create_production_containers { } +function create_release_containers { + BRANCH=$1 + + # Pull images if available for caching + docker pull "${CONTAINER_REPO}"/"${CONTAINER_APP}":builder ||: + docker pull "${CONTAINER_REPO}"/"${CONTAINER_APP}":production ||: + docker pull "${CONTAINER_REPO}"/"${CONTAINER_APP}":testing ||: + docker pull "${CONTAINER_REPO}"/"${CONTAINER_APP}":development ||: + + # Build the builder image + docker build --target builder \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \ + --tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":builder . + + # Build the production image + docker build --target production \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":production \ + --tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":${BUILDKITE_COMMIT:0:7} \ + --tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":${BRANCH} . + + # Build the testing image + docker build --target testing \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":production \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":testing \ + --tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":testing-${BRANCH} . + + # Build the development image + docker build --target development \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":production \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":testing \ + --tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":development-${BRANCH} . + + # If the env var for the git tag doesn't exist or is an empty string, then we + # won't build a container image for a cut release. + if [ -v BUILDKITE_TAG || ! -z "${BUILDKITE_TAG}" ]; then + docker build --target production \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \ + --cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":production \ + --tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":${BUILDKITE_TAG} . + fi + + # Push images to Quay + docker push "${CONTAINER_REPO}"/"${CONTAINER_APP}":${BRANCH} + docker push "${CONTAINER_REPO}"/"${CONTAINER_APP}":development-${BRANCH} + docker push "${CONTAINER_REPO}"/"${CONTAINER_APP}":testing-${BRANCH} + +} function prune_containers { @@ -111,6 +161,11 @@ function prune_containers { trap prune_containers ERR INT EXIT +echo "Branch: $BUILDKITE_BRANCH" +echo "PR : $BUILDKITE_PULL_REQUEST" +echo "Commit: $BUILDKITE_COMMIT" +echo "Tag : $BUILDKITE_TAG" + if [ ! -v BUILDKITE_BRANCH ]; then echo "Not running in Buildkite. Building Production Containers..." @@ -127,6 +182,11 @@ elif [[ "${BUILDKITE_BRANCH}" = "master" || "${BUILDKITE_BRANCH}" = "main" ]]; t echo "Building Production Containers..." create_production_containers +elif [[ ${BUILDKITE_BRANCH} = stable* ]]; then + + echo "Building Production Containers for ${BUILDKITE_BRANCH}..." + create_release_containers "${BUILDKITE_BRANCH}" + else if [ ! -v BUILDKITE_PULL_REQUEST ]; then diff --git a/scripts/release b/scripts/release new file mode 100755 index 000000000..b6148ce68 --- /dev/null +++ b/scripts/release @@ -0,0 +1,93 @@ +#!/usr/bin/env ruby + +require "date" +require "optparse" + +def run(command, abort_on_fail: true) + puts "> #{command}" if $VERBOSE + + system(command, exception: abort_on_fail) +rescue StandardError => e + raise "#{command.inspect} failed: #{e}" +end + +channel = ENV.fetch("FOREM_RELEASE_CHANNEL", "stable") +date = ENV.fetch("FOREM_RELEASE_DATE", Date.today.to_s).delete("-") +hotfix = ENV.fetch("FOREM_RELEASE_HOTFIX", 0) +remote = ENV.fetch("FOREM_RELEASE_REMOTE", "origin") +source = ENV.fetch("FOREM_STAGING_FROM", "#{channel}-next") + +OptionParser.new ARGV.dup do |options| + options.banner = <<~USAGE + Usage: #{$PROGRAM_NAME} [options] + + Releases are named CHANNEL.DATE.HOTFIX_COUNT. Example: #{channel}.#{date}.#{hotfix} + + USAGE + + options.on "-c", "--channel CHANNEL", + "Specify the release channel (defaults to #{channel.inspect})" do |release_channel| + channel = release_channel + end + + options.on "-d", "--date DATE", "Specify the release date (defaults to today)" do |release_date| + date = release_date.delete("-") + end + + options.on "-f", "--from BRANCH/COMMIT/TAG", + "Specify the source branch for this release (defaults to #{source.inspect})" do |from_branch| + source = from_branch + end + + options.on "-h", "--hotfix HOTFIX", + "Specify the hotfix count for this release branch (defaults to #{hotfix.inspect})" do |hotfix_opt| + hotfix = hotfix_opt + end + + options.on "-r", "--remote REMOTE", "Git remote to push to (defaults to #{remote.inspect})" do |git_remote| + remote = git_remote + end + + options.on "-v", "--verbose", "Enable verbose mode" do + $VERBOSE = true + end +end.parse! + +original_branch = `git branch --show-current`.strip + +begin + tag = "#{channel}.#{date}.#{hotfix}" + puts "Building #{tag}..." + + [source, channel].each do |branch| + run "git checkout --quiet #{branch}" + run "git pull --quiet --ff-only #{remote} #{branch}" + puts "Pulled latest #{branch.inspect}" + end + + run "git merge --quiet #{source} --no-ff --message 'Merge #{source} into #{channel}'" + puts "Merged #{source} into #{channel}" + + begin + run "git tag #{tag}" + puts "Tagged #{tag}" + rescue StandardError + # If `git tag` fails, it's because the tag already exists. In this case, git + # will have already output an error that looks like this: + # + # fatal: tag 'stable.20210512.0' already exists + # + warn "Should the hotfix count be set to something other than #{hotfix}?" \ + "Or maybe you need to delete the tag locally (hint: `git tag --delete #{tag}`)" + exit 1 + end + + run "git push --quiet #{remote} #{channel}" + puts "Pushed #{channel} to #{remote}" + + run "git push --quiet #{remote} #{tag}" + puts "Pushed #{tag} to #{remote}" +ensure + # Go back to the branch we were on before running this script + run "git checkout --quiet #{original_branch}" +end diff --git a/scripts/stage_release b/scripts/stage_release new file mode 100755 index 000000000..6cbec73d1 --- /dev/null +++ b/scripts/stage_release @@ -0,0 +1,64 @@ +#!/usr/bin/env ruby + +require "date" +require "optparse" + +def run(command, abort_on_fail: true) + puts "> #{command}" if $VERBOSE + + system(command, exception: abort_on_fail) +rescue StandardError => e + raise "#{command.inspect} failed: #{e}" +end + +remote = ENV.fetch("FOREM_RELEASE_REMOTE", "origin") +source = ENV.fetch("FOREM_RELEASE_FROM", "main") +target = "#{ENV.fetch('FOREM_RELEASE_CHANNEL', 'stable')}-next" + +OptionParser.new ARGV.dup do |options| + options.banner = <<~USAGE + Usage: #{$PROGRAM_NAME} [options] + + Staging banches are named CHANNEL-next. Example: #{target} + + USAGE + + options.on "-c", "--channel CHANNEL", + "Specify the release channel (defaults to #{target.sub(/-next$/, '').inspect})" do |release_channel| + target = "#{release_channel}-next" + end + + options.on "-f", "--from BRANCH/COMMIT/TAG", + "Specify the source branch for this release (defaults to #{source.inspect})" do |from_branch| + source = from_branch + end + + options.on "-r", "--remote REMOTE", "Git remote to push to (defaults to #{remote.inspect})" do |git_remote| + remote = git_remote + end + + options.on "-v", "--verbose", "Enable verbose mode" do + $VERBOSE = true + end +end.parse! + +original_branch = `git branch --show-current`.strip + +begin + puts "Fetching from #{remote}..." + [source, target].each do |branch| + run "git checkout --quiet #{branch}" + run "git pull --ff-only --quiet #{remote} #{branch}" + puts "Pulled latest #{branch.inspect}" + end + + run "git merge --quiet #{source}" + puts "Merged latest #{source} into #{target}" + + run "git push --quiet #{remote} #{target}" + puts "Pushed #{target}, run the following to release:" + puts " scripts/release --channel #{target.sub(/-next$/, '')} --remote #{remote} --from #{target}" +ensure + # Go back to the branch we were on before running this script + run "git checkout --quiet #{original_branch}" +end