fa-harness-tools 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8b62816b52a8c8cbb615df53ded387c2af166da2f3d986973d7649514ee17de
4
- data.tar.gz: 3274cc9cd5dfad6ee8a10fb2e8dc79856e83b42d16d368aa8466bddc150a3d46
3
+ metadata.gz: 51253c635049f814f42fcac831fd937ed713254a29547cf5915fc2861848a52e
4
+ data.tar.gz: ed786f440ef207a1f2d6b4c920d82f673c1726c81571f4d0cec692f02a2c42a4
5
5
  SHA512:
6
- metadata.gz: 5c0e753eab7662c459df2940436b57fbb2131710d4b572e3e87fc18f27315420c1e73635e9164ef20b7fb5e4486e871508e932fac69f1a013c988e1b77753ada
7
- data.tar.gz: 8fe5898455f1cf835978ea810aeb1924fa75318a547590670f982d1d0e624ec04a877eca96287ef2d7536e58553083f6e5bffd35ee0bc39cd6f864f4b057d2a7
6
+ metadata.gz: 3cb5bc18ffb6355a629de71c8220f851e12a85814e59549f8cfd7b259d4425d4422f81e0d85dc2e80e05e9fa76c5d5b3dfb12fe161fc61a9b87550dbd61300af
7
+ data.tar.gz: 73877cc9224058d573a41b3ab1b34e67d0fe8bbf06c17a408b37f646d552d7b627fab219d2b042654297b9ccb9dae09fbc4f1d833a6fae3e798b3603970fc320
@@ -1,8 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fa-harness-tools (1.0.2)
4
+ fa-harness-tools (1.2.0)
5
+ fugit (~> 1.3)
5
6
  octokit (~> 4.0)
7
+ pastel (~> 0.7)
6
8
  tzinfo (~> 2.0)
7
9
  tzinfo-data (~> 1.0)
8
10
 
@@ -11,15 +13,26 @@ GEM
11
13
  specs:
12
14
  addressable (2.7.0)
13
15
  public_suffix (>= 2.0.2, < 5.0)
14
- concurrent-ruby (1.1.5)
16
+ concurrent-ruby (1.1.6)
15
17
  diff-lcs (1.3)
16
- faraday (0.17.0)
18
+ equatable (0.6.1)
19
+ et-orbi (1.2.4)
20
+ tzinfo
21
+ faraday (1.0.1)
17
22
  multipart-post (>= 1.2, < 3)
23
+ fugit (1.3.5)
24
+ et-orbi (~> 1.1, >= 1.1.8)
25
+ raabro (~> 1.1)
18
26
  multipart-post (2.1.1)
19
- octokit (4.14.0)
27
+ octokit (4.18.0)
28
+ faraday (>= 0.9)
20
29
  sawyer (~> 0.8.0, >= 0.5.3)
21
- public_suffix (4.0.1)
22
- rake (10.5.0)
30
+ pastel (0.7.4)
31
+ equatable (~> 0.6)
32
+ tty-color (~> 0.5)
33
+ public_suffix (4.0.5)
34
+ raabro (1.3.1)
35
+ rake (13.0.1)
23
36
  rspec (3.9.0)
24
37
  rspec-core (~> 3.9.0)
25
38
  rspec-expectations (~> 3.9.0)
@@ -37,9 +50,10 @@ GEM
37
50
  addressable (>= 2.3.5)
38
51
  faraday (> 0.8, < 2.0)
39
52
  timecop (0.9.1)
40
- tzinfo (2.0.0)
53
+ tty-color (0.5.1)
54
+ tzinfo (2.0.2)
41
55
  concurrent-ruby (~> 1.0)
42
- tzinfo-data (1.2019.3)
56
+ tzinfo-data (1.2020.1)
43
57
  tzinfo (>= 1.0.0)
44
58
 
45
59
  PLATFORMS
@@ -48,9 +62,9 @@ PLATFORMS
48
62
  DEPENDENCIES
49
63
  bundler (~> 1.0)
50
64
  fa-harness-tools!
51
- rake (~> 10.0)
65
+ rake (~> 13.0)
52
66
  rspec (~> 3.8)
53
67
  timecop (~> 0.9)
54
68
 
55
69
  BUNDLED WITH
56
- 1.16.4
70
+ 1.17.2
data/README.md CHANGED
@@ -22,9 +22,11 @@ Or install it yourself as:
22
22
 
23
23
  Examples below use [variables defined by Harness](https://docs.harness.io/article/9dvxcegm90-variables) so should be suitable to use directly in Harness scripts.
24
24
 
25
- ### Required environment variables
25
+ Full scripts that can be used in Harness are available in the [examples/](examples/) directory.
26
26
 
27
- * `GITHUB_OAUTH_TOKEN` must be exported, containing a valid [personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) for GitHub
27
+ ### Optional environment variables
28
+
29
+ * To access private repositories, `GITHUB_OAUTH_TOKEN` must be exported, containing a valid [personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) for GitHub
28
30
 
29
31
  ### check-branch-brotection
30
32
 
@@ -0,0 +1,17 @@
1
+ # Create deployment Git tags
2
+
3
+ ## Purpose
4
+
5
+ Can be added to a Harness pipeline to add a Git tag on every deployment. The tags can then be used by the other pre-flight checks.
6
+
7
+ ## Requirements
8
+
9
+ 1. Add a GitHub OAuth token to the Harness secrets manager, named `github-oauth-token`
10
+ 2. Assumes the artifact build number is the commit ID
11
+ 3. fa-harness-tools is installed on the Harness delegates (`gem install -v $VERSION fa-harness-tools`)
12
+
13
+ ## Installation
14
+
15
+ Add the script to the Harness template library and then add to the last phase of the deployment workflow.
16
+
17
+ Define the `ONLY_ENVIRONMENT` variable input on the template, defaulting to `false`.
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ #
3
+ # Creates a Git tag to mark successful deployment with fa-harness-tools
4
+ #
5
+ # Optionally set ONLY_ENVIRONMENT to only tag when running a deployment in
6
+ # that Harness environment.
7
+
8
+ set -e
9
+
10
+ export GITHUB_OAUTH_TOKEN="${secrets.getValue("github-oauth-token")}"
11
+
12
+ if [ -z "${ONLY_ENVIRONMENT}" -o "${ONLY_ENVIRONMENT}" = "${env.name}" ]; then
13
+ create-deployment-tag \
14
+ --build-no "${artifact.buildNo}" \
15
+ --environment "${env.name}" \
16
+ --repository "${artifact.source.repositoryName}" \
17
+ --tagger-email "noreply@example.com" \
18
+ --tagger-name "Harness"
19
+ fi
@@ -0,0 +1,20 @@
1
+ # Production pre-flight checks
2
+
3
+ ## Purpose
4
+
5
+ Can be added to a Harness pipeline to enforce a set of strict requirements for production deployments:
6
+
7
+ 1. Only deploy within the daily deployment window/schedule
8
+ 2. Only deploy builds from the master branch
9
+ 3. Automated (triggered) deployments may only deploy forwards
10
+ 4. Manual deployments may only deploy forwards or roll back three deployments
11
+
12
+ ## Requirements
13
+
14
+ 1. Add a GitHub OAuth token to the Harness secrets manager, named `github-oauth-token`
15
+ 2. Assumes the artifact build number is the commit ID
16
+ 3. fa-harness-tools is installed on the Harness delegates (`gem install -v $VERSION fa-harness-tools`)
17
+
18
+ ## Installation
19
+
20
+ Add the script to the Harness template library and then add to an early phase of the deployment workflow.
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+ #
3
+ # Runs production deployment checks from fa-harness-tools
4
+
5
+ set -e
6
+
7
+ export GITHUB_OAUTH_TOKEN="${secrets.getValue("github-oauth-token")}"
8
+
9
+ run() {
10
+ CMD=$1
11
+ shift
12
+ echo
13
+ $CMD \
14
+ --build-no "${artifact.buildNo}" \
15
+ --environment "${env.name}" \
16
+ --repository "${artifact.source.repositoryName}" \
17
+ "$@"
18
+ echo
19
+ }
20
+
21
+ # 1. Check we're within the daily deployment schedule
22
+ check-schedule
23
+
24
+ # 2. Check the commit is on the master branch
25
+ run check-branch-protection
26
+
27
+ if [[ "${deploymentTriggeredBy}" =~ "Deployment Trigger" ]]; then
28
+ # 3. For automated deployments (trigger from CI), check deployment is fast-forward
29
+ run check-forward-deploy
30
+ else
31
+ # 3. For user deployments, check deployment is fast-forward or within last three deployments for rollbacks
32
+ run check-recent-deploy --allowed-rollback-count 3
33
+ fi
34
+
35
+ exit 0
@@ -32,7 +32,7 @@ OptionParser.new do |opts|
32
32
  end.parse!
33
33
 
34
34
  client = FaHarnessTools::GithubClient.new(
35
- oauth_token: ENV.fetch("GITHUB_OAUTH_TOKEN"),
35
+ oauth_token: ENV.fetch("GITHUB_OAUTH_TOKEN", nil),
36
36
  owner: options.fetch(:github_owner),
37
37
  repo: options.fetch(:repo),
38
38
  )
@@ -48,10 +48,4 @@ result = FaHarnessTools::CheckBranchProtection.new(
48
48
  branch: options.fetch(:branch),
49
49
  ).verify?
50
50
 
51
- if result.first
52
- puts result.last
53
- exit 0
54
- else
55
- $stderr.puts result.last
56
- exit 1
57
- end
51
+ exit result ? 0 : 1
@@ -48,10 +48,4 @@ result = FaHarnessTools::CheckForwardDeploy.new(
48
48
  tag_prefix: options.fetch(:tag_prefix),
49
49
  ).verify?
50
50
 
51
- if result.first
52
- puts result.last
53
- exit 0
54
- else
55
- $stderr.puts result.last
56
- exit 1
57
- end
51
+ exit result ? 0 : 1
@@ -54,10 +54,4 @@ result = FaHarnessTools::CheckRecentDeploy.new(
54
54
  allowed_rollback_count: options.fetch(:allowed_rollback_count),
55
55
  ).verify?
56
56
 
57
- if result.first
58
- puts result.last
59
- exit 0
60
- else
61
- $stderr.puts result.last
62
- exit 1
63
- end
57
+ exit result ? 0 : 1
@@ -3,17 +3,32 @@
3
3
  require "fa-harness-tools"
4
4
  require "optparse"
5
5
 
6
- options = {}
6
+ options = {
7
+ schedules: [],
8
+ timezone: "Europe/London",
9
+ }
7
10
  OptionParser.new do |opts|
8
11
  opts.banner = "Usage: check-schedule [options]"
9
- end.parse!
10
12
 
11
- result = FaHarnessTools::CheckSchedule.new.verify?
13
+ opts.on("--schedule CRON", "Schedule defines a window in which a deployment can take place. Accepts cron syntax, e.g. '* 9-15 * * mon-thu'") do |v|
14
+ options[:schedules] << v
15
+ end
16
+
17
+ opts.on("--timezone TIMEZONE", "Specify the timezone which the schedule will be checked in, e.g. 'Europe/London'") do |v|
18
+ options[:timezone] = v
19
+ end
20
+ end.parse!
12
21
 
13
- if result.first
14
- puts result.last
15
- exit 0
16
- else
17
- $stderr.puts result.last
18
- exit 1
22
+ def schedules(options)
23
+ if options[:schedules].length == 0
24
+ options[:schedules] = [
25
+ "* 9-15 * * mon-thu",
26
+ "* 9-11 * * fri",
27
+ ]
28
+ end
29
+ options[:schedules].map { |schedule| FaHarnessTools::Schedule.new(schedule: schedule.to_s) }
19
30
  end
31
+
32
+ result = FaHarnessTools::CheckSchedule.new(timezone: options[:timezone], schedules: schedules(options)).verify?
33
+
34
+ exit result ? 0 : 1
@@ -27,11 +27,13 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ["lib"]
28
28
 
29
29
  spec.add_runtime_dependency "octokit", "~> 4.0"
30
+ spec.add_runtime_dependency "pastel", "~> 0.7"
30
31
  spec.add_runtime_dependency "tzinfo", "~> 2.0"
31
32
  spec.add_runtime_dependency "tzinfo-data", "~> 1.0"
33
+ spec.add_runtime_dependency "fugit", "~> 1.3"
32
34
 
33
35
  spec.add_development_dependency "bundler", "~> 1.0"
34
- spec.add_development_dependency "rake", "~> 10.0"
36
+ spec.add_development_dependency "rake", "~> 13.0"
35
37
  spec.add_development_dependency "rspec", "~> 3.8"
36
38
  spec.add_development_dependency "timecop", "~> 0.9"
37
39
  end
@@ -1,11 +1,14 @@
1
+ require "fa-harness-tools/check_logger"
1
2
  require "fa-harness-tools/check_branch_protection"
2
3
  require "fa-harness-tools/check_forward_deploy"
3
4
  require "fa-harness-tools/check_recent_deploy"
4
5
  require "fa-harness-tools/check_schedule"
6
+ require "fa-harness-tools/schedule"
5
7
  require "fa-harness-tools/github_client"
6
8
  require "fa-harness-tools/harness_context"
7
9
  require "fa-harness-tools/version"
8
10
 
9
11
  module FaHarnessTools
10
12
  LookupError = Class.new(StandardError)
13
+ InvalidScheduleError = Class.new(StandardError)
11
14
  end
@@ -5,14 +5,23 @@ module FaHarnessTools
5
5
  @client = client
6
6
  @context = context
7
7
  @branch = branch
8
+ @logger = CheckLogger.new(
9
+ name: "Check branch protection",
10
+ description: "Only allow commits on the #{@branch} branch to be deployed",
11
+ )
8
12
  end
9
13
 
10
14
  def verify?
15
+ @logger.start
16
+ @logger.context_info(@client, @context)
17
+
11
18
  new_sha = @context.new_commit_sha
19
+
20
+ @logger.info("checking if #{@branch} branch contains the commit")
12
21
  if @client.branch_contains?(@branch, new_sha)
13
- [true, "#{@branch} contains #{new_sha}"]
22
+ @logger.pass "#{@branch} contains #{new_sha}"
14
23
  else
15
- [false, "#{@branch} does not contain #{new_sha}"]
24
+ @logger.fail "#{@branch} does not contain #{new_sha}"
16
25
  end
17
26
  end
18
27
  end
@@ -13,25 +13,36 @@ module FaHarnessTools
13
13
  @client = client
14
14
  @context = context
15
15
  @tag_prefix = tag_prefix
16
+ @logger = CheckLogger.new(
17
+ name: "Check forward deploy",
18
+ description: "Only allow deployments that are newer than what's currently deployed",
19
+ )
16
20
  end
17
21
 
18
22
  def verify?
23
+ @logger.start
24
+ @logger.context_info(@client, @context)
25
+
19
26
  current_tag = @client.last_deploy_tag(
20
27
  prefix: @tag_prefix, environment: @context.environment)
21
28
 
22
29
  if current_tag.nil?
23
30
  # If no previous deploys we need to let it deploy otherwise it will
24
31
  # never get past this check!
25
- return true, "first deploy"
32
+ @logger.info "no #{@tag_prefix} tag was found, so this must be the first deployment"
33
+ return @logger.pass("this is the first recorded deployment so is permitted")
26
34
  end
27
35
 
36
+ @logger.info("the most recent deployment is #{current_tag[:name]}")
37
+
28
38
  current_deployed_rev = current_tag[:commit][:sha]
29
39
  rev = @context.new_commit_sha
40
+ @logger.info("which means the currently deployed commit is #{current_deployed_rev}")
30
41
 
31
42
  if @client.is_ancestor_of?(current_deployed_rev, rev)
32
- [true, "forward deploy, #{rev} is ahead of #{current_deployed_rev}"]
43
+ @logger.pass "the commit being deployed is more recent than the currently deployed commit"
33
44
  else
34
- [false, "not a forward deploy, #{rev} is behind #{current_deployed_rev}"]
45
+ @logger.fail "the commit being deployed is before the currently deployed commit, so would revert changes"
35
46
  end
36
47
  end
37
48
  end
@@ -0,0 +1,34 @@
1
+ require "pastel"
2
+
3
+ module FaHarnessTools
4
+ class CheckLogger
5
+ def initialize(name:, description:)
6
+ @name = name
7
+ @description = description
8
+ @pastel = Pastel.new(enabled: true)
9
+ end
10
+
11
+ def start
12
+ puts @pastel.cyan(@pastel.bold(@name), %{ (#{@description})})
13
+ end
14
+
15
+ def info(message)
16
+ puts " ... #{message}"
17
+ end
18
+
19
+ def context_info(client, context)
20
+ info("we're deploying repo #{client.owner_repo} into environment #{context.environment}")
21
+ info("we're trying to deploy commit #{context.new_commit_sha}")
22
+ end
23
+
24
+ def pass(message)
25
+ puts @pastel.green("PASS: #{message}")
26
+ true
27
+ end
28
+
29
+ def fail(message)
30
+ puts @pastel.red("FAIL: #{message}")
31
+ false
32
+ end
33
+ end
34
+ end
@@ -27,28 +27,39 @@ module FaHarnessTools
27
27
  @context = context
28
28
  @tag_prefix = tag_prefix
29
29
  @allowed_rollback_count = allowed_rollback_count
30
+ @logger = CheckLogger.new(
31
+ name: "Check recent deploys",
32
+ description: "Only allow deployments of recent commits, up to #{@allowed_rollback_count} deployment rollbacks",
33
+ )
30
34
  end
31
35
 
32
36
  def verify?
37
+ @logger.start
38
+ @logger.context_info(@client, @context)
39
+
33
40
  tags = @client.
34
41
  all_deploy_tags(prefix: @tag_prefix, environment: @context.environment).
35
42
  sort_by { |tag| tag[:name] }
36
43
 
37
- latest_allowed_tag = tags[@allowed_rollback_count * -1]
44
+ latest_allowed_tag = tags.last(@allowed_rollback_count).first
38
45
 
39
46
  if latest_allowed_tag.nil?
40
47
  # If no previous deploys we need to let it deploy otherwise it will
41
48
  # never get past this check!
42
- return true, "first deploy"
49
+ @logger.info "no #{@tag_prefix} tag was found, so this must be the first deployment"
50
+ return @logger.pass("this is the first recorded deployment so is permitted")
43
51
  end
44
52
 
45
- latest_allowed_rev = latest_allowed_tag[:commit][:sha]
53
+ @logger.info("the most recent tag allowed is #{latest_allowed_tag[:name]}")
54
+
55
+ latest_allowed_rev = @client.get_commit_sha_from_tag(latest_allowed_tag)
46
56
  rev = @context.new_commit_sha
57
+ @logger.info("which means the most recent commit allowed is #{latest_allowed_rev}")
47
58
 
48
59
  if @client.is_ancestor_of?(latest_allowed_rev, rev)
49
- [true, "#{rev} is ahead of no.#{@allowed_rollback_count} most recent commit with #{@tag_prefix.inspect} tag"]
60
+ @logger.pass "the commit being deployed is more recent than the last permitted rollback commit"
50
61
  else
51
- [false, "#{rev} is prior to no.#{@allowed_rollback_count} most recent commit with #{@tag_prefix.inspect} tag"]
62
+ @logger.fail "the commit being deployed is older than the last permitted rollback commit"
52
63
  end
53
64
  end
54
65
  end
@@ -3,28 +3,35 @@ require "tzinfo"
3
3
 
4
4
  module FaHarnessTools
5
5
  # Check against the time of day so you can restrict deploying to sensible
6
- # hours. Uses local London time by default.
7
- #
8
- # Restricts to Mon-Thu from 9am to 4pm, Fri from 9am to 12pm.
6
+ # hours.
9
7
  class CheckSchedule
10
- def initialize(timezone: "Europe/London")
8
+ def initialize(timezone:, schedules:)
11
9
  tz = TZInfo::Timezone.get(timezone)
10
+ @timezone = timezone
12
11
  @now = tz.to_local(Time.now.utc)
12
+ @schedules = schedules
13
+ @logger = CheckLogger.new(
14
+ name: "Check deployment schedule",
15
+ description: "Only allow deployments within certain times of the day",
16
+ )
13
17
  end
14
18
 
15
19
  def verify?
16
- permitted = false
17
- case @now.wday
18
- when 1..4
19
- permitted = true if @now.hour >= 9 && @now.hour < 16
20
- when 5
21
- permitted = true if @now.hour >= 9 && @now.hour < 12
20
+ @logger.start
21
+ @logger.info("operating in the #{@timezone} timezone")
22
+ @logger.info("local time is #{@now}")
23
+
24
+ permitted = @schedules.any? do |schedule|
25
+ can_run = schedule.can_run?(time: @now)
26
+ @logger.info("deployments are allowed due to the following schedule: #{schedule.to_s}") if can_run
27
+ can_run
22
28
  end
23
29
 
24
30
  if permitted
25
- [true, "scheduled deploy time"]
31
+ @logger.pass "inside the deployment schedule"
26
32
  else
27
- [false, "outside deployment schedule"]
33
+ @logger.info "failed to match any schedule #{@schedules.map(&:to_s).join(", ")}"
34
+ @logger.fail "outside the deployment schedule"
28
35
  end
29
36
  end
30
37
  end
@@ -9,6 +9,8 @@ module FaHarnessTools
9
9
  @octokit = Octokit::Client.new(access_token: oauth_token)
10
10
  @owner = owner
11
11
  @repo = repo
12
+ @oauth_token = oauth_token
13
+ validate_repo
12
14
  end
13
15
 
14
16
  def owner_repo
@@ -17,14 +19,24 @@ module FaHarnessTools
17
19
 
18
20
  # Return all tags starting "harness-deploy-ENV-"
19
21
  #
20
- # Used to find deployments in an environment. The commit SHA of the tag is
21
- # in [:commit][:sha] in the returned hash.
22
+ # Used to find deployments in an environment. Provides only the tag name
23
+ # and object, though that may be an annotated tag or a commit.
24
+ #
25
+ # Use #get_commit_sha_from_tag to reliably find the commit that a tag
26
+ # points to.
22
27
  #
23
28
  # @return [Array[Hash]] Array of tag data hash, or [] if none
24
29
  def all_deploy_tags(prefix:, environment:)
25
- @octokit.tags(owner_repo).find_all do |tag|
26
- tag[:name].start_with?("#{prefix}-#{environment}-")
30
+ # #refs is a much quicker way than #tags to pull back all tag names, so
31
+ # we prefer this and then fetch commit information only when we need it
32
+ @octokit.refs(owner_repo, "tags/#{prefix}-#{environment}-").map do |ref|
33
+ {
34
+ name: ref[:ref][10..-1], # remove refs/tags/ prefix
35
+ object: ref[:object],
36
+ }
27
37
  end
38
+ rescue Octokit::NotFound
39
+ []
28
40
  end
29
41
 
30
42
  # Return the last (when sorted) tag starting "harness-deploy-ENV-"
@@ -34,8 +46,13 @@ module FaHarnessTools
34
46
  #
35
47
  # @return [Hash] Tag data hash, or nil if none
36
48
  def last_deploy_tag(prefix:, environment:)
37
- last_tag = all_deploy_tags.sort_by { |tag| tag[:name] }.last
38
- last_tag ? last_tag : nil
49
+ last_tag = all_deploy_tags(prefix: prefix, environment: environment).
50
+ sort_by { |tag| tag[:name] }.last
51
+ return nil unless last_tag
52
+
53
+ last_tag.merge(
54
+ commit: { sha: get_commit_sha_from_tag(last_tag) },
55
+ )
39
56
  end
40
57
 
41
58
  # Return a full commit SHA from a short SHA
@@ -48,20 +65,48 @@ module FaHarnessTools
48
65
  commit[:sha]
49
66
  end
50
67
 
68
+ # Return a full commit SHA from a tag
69
+ #
70
+ # The `tag` argument should be a Hash of tag data with an :object that can
71
+ # either be an annotated tag or a commit object.
72
+ #
73
+ # @return [String] Full commit SHA
74
+ # @raise [LookupError] If tag cannot be found
75
+ def get_commit_sha_from_tag(tag)
76
+ case tag[:object][:type]
77
+ when "commit"
78
+ tag[:object][:sha]
79
+ when "tag"
80
+ # When a tag points to a tag, recurse into it until we find a commit object
81
+ refed_tag = @octokit.tag(owner_repo, tag[:object][:sha])
82
+ get_commit_sha_from_tag(refed_tag.to_h.merge(tag.slice(:name)))
83
+ else
84
+ raise LookupError, "Tag #{tag[:name]} points to a non-commit object (#{tag[:object].inspect})"
85
+ end
86
+ rescue Octokit::NotFound
87
+ raise LookupError, "Unable to find tag #{tag.inspect} in Git repo"
88
+ end
89
+
51
90
  # Checks if <ancestor> is an ancestor of <commit>
52
91
  #
53
92
  # i.e. commit and ancestor are directly related
54
93
  #
55
94
  # @return [Bool] True is <ancestor> is ancestor of <commit>
56
95
  def is_ancestor_of?(ancestor, commit)
57
- !!@octokit.commits(owner_repo, commit).find { |c| c[:sha] == ancestor }
96
+ # Compare returns the merge base, the common point in history between the
97
+ # two commits. If X is the ancestor of Y, then the merge base must be X.
98
+ # If not, it's a different branch.
99
+ @octokit.compare(owner_repo, ancestor, commit)[:merge_base_commit][:sha] == get_commit_sha(ancestor)
58
100
  end
59
101
 
60
102
  # Checks if <commit> is on branch <branch>
61
103
  #
62
104
  # @return [Bool] True is <commit> is on <branch>
63
105
  def branch_contains?(branch, commit)
64
- !!@octokit.commits(owner_repo, branch).find { |c| c[:sha] == commit }
106
+ # The same implementation works for this question. We have both methods
107
+ # to make the intent clearer and also this one is guaranteed to resolve a
108
+ # branch name for the first argument.
109
+ is_ancestor_of?(commit, branch)
65
110
  end
66
111
 
67
112
  # Creates a Git tag
@@ -72,5 +117,18 @@ module FaHarnessTools
72
117
  @octokit.create_ref(owner_repo, "tags/#{tag}", commit_sha)
73
118
  @octokit.create_tag(owner_repo, tag, message, commit_sha, *args)
74
119
  end
120
+
121
+ private
122
+
123
+ # Validates a repository exists.
124
+ # Raises a `LookupError` in the event a repository can't be found.
125
+ def validate_repo
126
+ @octokit.repo(owner_repo)
127
+
128
+ rescue Octokit::NotFound
129
+ message = "Unable to find repository #{owner_repo}"
130
+ message = "#{message}. If the repository is private, try setting GITHUB_OAUTH_TOKEN" unless @oauth_token
131
+ raise LookupError, message
132
+ end
75
133
  end
76
134
  end
@@ -9,7 +9,7 @@ module FaHarnessTools
9
9
  end
10
10
 
11
11
  def new_commit_sha
12
- @client.get_commit_sha(@build_no)
12
+ @new_commit_sha ||= @client.get_commit_sha(@build_no)
13
13
  end
14
14
  end
15
15
  end
@@ -0,0 +1,22 @@
1
+ require 'fugit'
2
+
3
+ module FaHarnessTools
4
+ # Creates a schedule which can be used to check if a change
5
+ # should be deployed.
6
+ class Schedule
7
+ def initialize(schedule:)
8
+ @schedule = schedule
9
+ @cron_schedule = Fugit.parse(schedule)
10
+ raise InvalidScheduleError, "'#{schedule}' can not be parsed" unless @cron_schedule
11
+ end
12
+
13
+ def can_run?(time:)
14
+ return false unless @cron_schedule.day_match?(time)
15
+ return @cron_schedule.hour_match?(time)
16
+ end
17
+
18
+ def to_s
19
+ return @schedule
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module FaHarnessTools
2
- VERSION = "1.0.2"
2
+ VERSION = "1.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fa-harness-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - FreeAgent
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-11-11 00:00:00.000000000 Z
11
+ date: 2020-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: octokit
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pastel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.7'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: tzinfo
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: fugit
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: bundler
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +100,14 @@ dependencies:
72
100
  requirements:
73
101
  - - "~>"
74
102
  - !ruby/object:Gem::Version
75
- version: '10.0'
103
+ version: '13.0'
76
104
  type: :development
77
105
  prerelease: false
78
106
  version_requirements: !ruby/object:Gem::Requirement
79
107
  requirements:
80
108
  - - "~>"
81
109
  - !ruby/object:Gem::Version
82
- version: '10.0'
110
+ version: '13.0'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: rspec
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -133,6 +161,10 @@ files:
133
161
  - Rakefile
134
162
  - bin/console
135
163
  - bin/setup
164
+ - examples/create-deployment-tag/README.md
165
+ - examples/create-deployment-tag/create-deployment-tag.sh
166
+ - examples/production-preflight-checks/README.md
167
+ - examples/production-preflight-checks/production-preflight-checks.sh
136
168
  - exe/check-branch-protection
137
169
  - exe/check-forward-deploy
138
170
  - exe/check-recent-deploy
@@ -142,10 +174,12 @@ files:
142
174
  - lib/fa-harness-tools.rb
143
175
  - lib/fa-harness-tools/check_branch_protection.rb
144
176
  - lib/fa-harness-tools/check_forward_deploy.rb
177
+ - lib/fa-harness-tools/check_logger.rb
145
178
  - lib/fa-harness-tools/check_recent_deploy.rb
146
179
  - lib/fa-harness-tools/check_schedule.rb
147
180
  - lib/fa-harness-tools/github_client.rb
148
181
  - lib/fa-harness-tools/harness_context.rb
182
+ - lib/fa-harness-tools/schedule.rb
149
183
  - lib/fa-harness-tools/version.rb
150
184
  homepage: https://github.com/fac/fa-harness-tools
151
185
  licenses: