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