Add support for versioned releases (#13750)

* Add release scripts

* Use `delete` over `gsub` with an empty string

Co-authored-by: Michael Kohl <citizen428@dev.to>

* 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 <citizen428@dev.to>
This commit is contained in:
Jamie Gaskins 2021-07-28 10:10:33 -04:00 committed by GitHub
parent 71fc58c497
commit f0edbcef90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 226 additions and 0 deletions

1
.gitattributes vendored
View file

@ -8,3 +8,4 @@
*.css diff=css
*.html diff=html
*.erb diff=html
/CHANGELOG.md merge=union

View file

@ -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

3
CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
## 2021-07-23
Initial versioned release

View file

@ -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"
],

View file

@ -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

93
scripts/release Executable file
View file

@ -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

64
scripts/stage_release Executable file
View file

@ -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