Skip to content

Commit f0edbce

Browse files
Jamie GaskinsMichael Kohl
Jamie Gaskins
and
Michael Kohl
authored
Add support for versioned releases (forem#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>
1 parent 71fc58c commit f0edbce

7 files changed

+226
-0
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
*.css diff=css
99
*.html diff=html
1010
*.erb diff=html
11+
/CHANGELOG.md merge=union

.github/PULL_REQUEST_TEMPLATE.md

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ will share this change with the people who need to know about it._
6060
[Admin Guide](https://admin.forem.com/), or
6161
[Storybook](https://storybook.forem.com/) (for Crayons components)
6262
- [ ] I've updated the README or added inline documentation
63+
- [ ] I've added an entry to
64+
[`CHANGELOG.md`](https://github.com/forem/forem/tree/main/CHANGELOG.md)
6365
- [ ] I will share this change in a [Changelog](https://forem.dev/t/changelog)
6466
or in a [forem.dev](http://forem.dev) post
6567
- [ ] I will share this change internally with the appropriate teams

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 2021-07-23
2+
3+
Initial versioned release

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
"{app,spec,config,lib}/**/*.rb": [
4545
"bundle exec rubocop --require rubocop-rspec --auto-correct"
4646
],
47+
"scripts/{release,stage_release}": [
48+
"bundle exec rubocop --require rubocop-rspec --auto-correct --except Style/TopLevelMethodDefinition,Rails/Date"
49+
],
4750
"app/views/**/*.jbuilder": [
4851
"bundle exec rubocop --require rubocop-rspec --auto-correct"
4952
],

scripts/build_containers.sh

+60
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,56 @@ function create_production_containers {
102102

103103
}
104104

105+
function create_release_containers {
106+
BRANCH=$1
107+
108+
# Pull images if available for caching
109+
docker pull "${CONTAINER_REPO}"/"${CONTAINER_APP}":builder ||:
110+
docker pull "${CONTAINER_REPO}"/"${CONTAINER_APP}":production ||:
111+
docker pull "${CONTAINER_REPO}"/"${CONTAINER_APP}":testing ||:
112+
docker pull "${CONTAINER_REPO}"/"${CONTAINER_APP}":development ||:
113+
114+
# Build the builder image
115+
docker build --target builder \
116+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \
117+
--tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":builder .
118+
119+
# Build the production image
120+
docker build --target production \
121+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \
122+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":production \
123+
--tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":${BUILDKITE_COMMIT:0:7} \
124+
--tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":${BRANCH} .
125+
126+
# Build the testing image
127+
docker build --target testing \
128+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \
129+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":production \
130+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":testing \
131+
--tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":testing-${BRANCH} .
132+
133+
# Build the development image
134+
docker build --target development \
135+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \
136+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":production \
137+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":testing \
138+
--tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":development-${BRANCH} .
139+
140+
# If the env var for the git tag doesn't exist or is an empty string, then we
141+
# won't build a container image for a cut release.
142+
if [ -v BUILDKITE_TAG || ! -z "${BUILDKITE_TAG}" ]; then
143+
docker build --target production \
144+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":builder \
145+
--cache-from="${CONTAINER_REPO}"/"${CONTAINER_APP}":production \
146+
--tag "${CONTAINER_REPO}"/"${CONTAINER_APP}":${BUILDKITE_TAG} .
147+
fi
148+
149+
# Push images to Quay
150+
docker push "${CONTAINER_REPO}"/"${CONTAINER_APP}":${BRANCH}
151+
docker push "${CONTAINER_REPO}"/"${CONTAINER_APP}":development-${BRANCH}
152+
docker push "${CONTAINER_REPO}"/"${CONTAINER_APP}":testing-${BRANCH}
153+
154+
}
105155

106156
function prune_containers {
107157

@@ -111,6 +161,11 @@ function prune_containers {
111161

112162
trap prune_containers ERR INT EXIT
113163

164+
echo "Branch: $BUILDKITE_BRANCH"
165+
echo "PR : $BUILDKITE_PULL_REQUEST"
166+
echo "Commit: $BUILDKITE_COMMIT"
167+
echo "Tag : $BUILDKITE_TAG"
168+
114169
if [ ! -v BUILDKITE_BRANCH ]; then
115170

116171
echo "Not running in Buildkite. Building Production Containers..."
@@ -127,6 +182,11 @@ elif [[ "${BUILDKITE_BRANCH}" = "master" || "${BUILDKITE_BRANCH}" = "main" ]]; t
127182
echo "Building Production Containers..."
128183
create_production_containers
129184

185+
elif [[ ${BUILDKITE_BRANCH} = stable* ]]; then
186+
187+
echo "Building Production Containers for ${BUILDKITE_BRANCH}..."
188+
create_release_containers "${BUILDKITE_BRANCH}"
189+
130190
else
131191

132192
if [ ! -v BUILDKITE_PULL_REQUEST ]; then

scripts/release

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env ruby
2+
3+
require "date"
4+
require "optparse"
5+
6+
def run(command, abort_on_fail: true)
7+
puts "> #{command}" if $VERBOSE
8+
9+
system(command, exception: abort_on_fail)
10+
rescue StandardError => e
11+
raise "#{command.inspect} failed: #{e}"
12+
end
13+
14+
channel = ENV.fetch("FOREM_RELEASE_CHANNEL", "stable")
15+
date = ENV.fetch("FOREM_RELEASE_DATE", Date.today.to_s).delete("-")
16+
hotfix = ENV.fetch("FOREM_RELEASE_HOTFIX", 0)
17+
remote = ENV.fetch("FOREM_RELEASE_REMOTE", "origin")
18+
source = ENV.fetch("FOREM_STAGING_FROM", "#{channel}-next")
19+
20+
OptionParser.new ARGV.dup do |options|
21+
options.banner = <<~USAGE
22+
Usage: #{$PROGRAM_NAME} [options]
23+
24+
Releases are named CHANNEL.DATE.HOTFIX_COUNT. Example: #{channel}.#{date}.#{hotfix}
25+
26+
USAGE
27+
28+
options.on "-c", "--channel CHANNEL",
29+
"Specify the release channel (defaults to #{channel.inspect})" do |release_channel|
30+
channel = release_channel
31+
end
32+
33+
options.on "-d", "--date DATE", "Specify the release date (defaults to today)" do |release_date|
34+
date = release_date.delete("-")
35+
end
36+
37+
options.on "-f", "--from BRANCH/COMMIT/TAG",
38+
"Specify the source branch for this release (defaults to #{source.inspect})" do |from_branch|
39+
source = from_branch
40+
end
41+
42+
options.on "-h", "--hotfix HOTFIX",
43+
"Specify the hotfix count for this release branch (defaults to #{hotfix.inspect})" do |hotfix_opt|
44+
hotfix = hotfix_opt
45+
end
46+
47+
options.on "-r", "--remote REMOTE", "Git remote to push to (defaults to #{remote.inspect})" do |git_remote|
48+
remote = git_remote
49+
end
50+
51+
options.on "-v", "--verbose", "Enable verbose mode" do
52+
$VERBOSE = true
53+
end
54+
end.parse!
55+
56+
original_branch = `git branch --show-current`.strip
57+
58+
begin
59+
tag = "#{channel}.#{date}.#{hotfix}"
60+
puts "Building #{tag}..."
61+
62+
[source, channel].each do |branch|
63+
run "git checkout --quiet #{branch}"
64+
run "git pull --quiet --ff-only #{remote} #{branch}"
65+
puts "Pulled latest #{branch.inspect}"
66+
end
67+
68+
run "git merge --quiet #{source} --no-ff --message 'Merge #{source} into #{channel}'"
69+
puts "Merged #{source} into #{channel}"
70+
71+
begin
72+
run "git tag #{tag}"
73+
puts "Tagged #{tag}"
74+
rescue StandardError
75+
# If `git tag` fails, it's because the tag already exists. In this case, git
76+
# will have already output an error that looks like this:
77+
#
78+
# fatal: tag 'stable.20210512.0' already exists
79+
#
80+
warn "Should the hotfix count be set to something other than #{hotfix}?" \
81+
"Or maybe you need to delete the tag locally (hint: `git tag --delete #{tag}`)"
82+
exit 1
83+
end
84+
85+
run "git push --quiet #{remote} #{channel}"
86+
puts "Pushed #{channel} to #{remote}"
87+
88+
run "git push --quiet #{remote} #{tag}"
89+
puts "Pushed #{tag} to #{remote}"
90+
ensure
91+
# Go back to the branch we were on before running this script
92+
run "git checkout --quiet #{original_branch}"
93+
end

scripts/stage_release

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env ruby
2+
3+
require "date"
4+
require "optparse"
5+
6+
def run(command, abort_on_fail: true)
7+
puts "> #{command}" if $VERBOSE
8+
9+
system(command, exception: abort_on_fail)
10+
rescue StandardError => e
11+
raise "#{command.inspect} failed: #{e}"
12+
end
13+
14+
remote = ENV.fetch("FOREM_RELEASE_REMOTE", "origin")
15+
source = ENV.fetch("FOREM_RELEASE_FROM", "main")
16+
target = "#{ENV.fetch('FOREM_RELEASE_CHANNEL', 'stable')}-next"
17+
18+
OptionParser.new ARGV.dup do |options|
19+
options.banner = <<~USAGE
20+
Usage: #{$PROGRAM_NAME} [options]
21+
22+
Staging banches are named CHANNEL-next. Example: #{target}
23+
24+
USAGE
25+
26+
options.on "-c", "--channel CHANNEL",
27+
"Specify the release channel (defaults to #{target.sub(/-next$/, '').inspect})" do |release_channel|
28+
target = "#{release_channel}-next"
29+
end
30+
31+
options.on "-f", "--from BRANCH/COMMIT/TAG",
32+
"Specify the source branch for this release (defaults to #{source.inspect})" do |from_branch|
33+
source = from_branch
34+
end
35+
36+
options.on "-r", "--remote REMOTE", "Git remote to push to (defaults to #{remote.inspect})" do |git_remote|
37+
remote = git_remote
38+
end
39+
40+
options.on "-v", "--verbose", "Enable verbose mode" do
41+
$VERBOSE = true
42+
end
43+
end.parse!
44+
45+
original_branch = `git branch --show-current`.strip
46+
47+
begin
48+
puts "Fetching from #{remote}..."
49+
[source, target].each do |branch|
50+
run "git checkout --quiet #{branch}"
51+
run "git pull --ff-only --quiet #{remote} #{branch}"
52+
puts "Pulled latest #{branch.inspect}"
53+
end
54+
55+
run "git merge --quiet #{source}"
56+
puts "Merged latest #{source} into #{target}"
57+
58+
run "git push --quiet #{remote} #{target}"
59+
puts "Pushed #{target}, run the following to release:"
60+
puts " scripts/release --channel #{target.sub(/-next$/, '')} --remote #{remote} --from #{target}"
61+
ensure
62+
# Go back to the branch we were on before running this script
63+
run "git checkout --quiet #{original_branch}"
64+
end

0 commit comments

Comments
 (0)