Forked from asterite/move_deployed_linear_issues.rb
Created
November 24, 2022 07:02
-
-
Save BarnabeD/9833b69437e7ca13d5b7ea9dbca6ebd5 to your computer and use it in GitHub Desktop.
Revisions
-
asterite created this gist
Aug 24, 2022 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,279 @@ # Make sure to configure this script! # 1. Change `TEAM_LINEAR` properties below to match your Linear team # 2. Optionally add more teams # 3. Change the value of `DEPLOYED_TO_STAGING_GIT_TAG` to the tag/branch you use for deploys to staging # 4. Change the value of `DEPLOYED_TO_PRODUCTION_GIT_TAG` to the tag/branch you use for deploys to production # # Usage: # LINEAR_API_KEY=... GITHUB_API_KEY=... ruby move_deployed_linear_issues.rb # # Adding new teams (change "LinearTeam" to your team name): # - fetch your team ID with this Graphql query: # query Teams { # teams(filter: {name: {eq: "LinearTeam"}}) { # nodes { # id # name # } # } # } # - fetch your workflow state ids with this Graphql query: # query WorkflowStates { # workflowStates(filter: {team: {name: {eq: "LinearTeam"}}}) { # nodes { # id # name # team { # name # } # } # } # } # - add your team to TEAMS below require "json" require "uri" require "net/http" class GraphqlClient def initialize(endpoint:, key:) @uri = URI.parse(endpoint) @key = key @http = Net::HTTP.new(@uri.host, @uri.port) end def query(query) response = Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == 'https') do |http| request = Net::HTTP::Post.new( @uri.request_uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{@key}", ) request.body = {query: query}.to_json http.request(request) end unless response.code.to_i == 200 raise "Got bad status code when requesting #{@uri}: #{response.code}" end json = JSON.parse(response.body) if json.key?("errors") raise "Got error on Graphql query: #{json}" end json end end PullRequest = Struct.new(:repo_name, :number, keyword_init: true) CommitDeployStatus = Struct.new(:staging, :production, keyword_init: true) class CommitDeployStatus def deployed? staging || production end end LinearIssue = Struct.new(:id, :number, :title, :state, :pull_requests, keyword_init: true) LinearState = Struct.new(:id, :name, keyword_init: true) Team = Struct.new(:id, :name, :merged_state, :staging_state, :production_state, keyword_init: true) TEAM_LINEAR = Team.new( id: "some team id", name: "some team name", merged_state: LinearState.new( id: "the id of the merged state column", name: "the name of the merged state column", ), # this one can be nil if your team doesn't have a staging column staging_state: LinearState.new( id: "the id of the 'staging' state column", name: "the name of the 'staging' state column", ), production_state: LinearState.new( id: "the id of the 'production' state column", name: "the name of the 'production' state column", ), ) TEAMS = [ TEAM_LINEAR, ].freeze DEPLOYED_TO_STAGING_GIT_TAG = "origin/the-tag-you-use-for-deploys-to-staging" DEPLOYED_TO_PRODUCTION_GIT_TAG = "origin/the-tag-you-use-for-deploys-to-production" class LinearWorkflowChanger attr_reader :linear attr_reader :github def initialize @linear = GraphqlClient.new( endpoint: "https://api.linear.app/graphql".freeze, key: ENV.fetch("LINEAR_API_KEY"), ) @github = GraphqlClient.new( endpoint: "https://api.github.com/graphql".freeze, key: ENV.fetch("GITHUB_API_KEY"), ) end def do_it `git fetch` TEAMS.each do |team| puts "Processing Team #{team.name} Linear issues..." do_it_for_team(team) end end def do_it_for_team(team) linear_merged_or_staging_issues(team).each do |issue| process_team_issue(team, issue) end end def linear_merged_or_staging_issues(team) state_names = [team.merged_state, team.staging_state] .compact .map(&:name) fetch_linear_issues(%( query Team { team(id: "#{team.id}") { issues(filter: { state: { name: { in: #{state_names} } } }) { nodes { id number title state { name } integrationResources { nodes { pullRequest { repoName number } } } } } } } )) end def process_team_issue(team, issue) return if issue.pull_requests.empty? oids = issue.pull_requests.map { |pr| fetch_github_pr_merge_commit_oid(pr) } # Bail out unless all PRs have been merged return unless oids.all? deploy_statuses = oids.map { |oid| commit_deploy_status(oid) } # If all PRs are in production, move the issue to "Done/Deployed" if deploy_statuses.all?(&:production) set_linear_issue_state(issue, team.production_state) return end staging_state = team.staging_state # Not all teams have a column for "In Staging" return unless staging_state # If the issue in staging already, it won't move anywhere else for now return if issue.state == staging_state.name # There's also nothing else to do unless all PRs are in staging return unless deploy_statuses.all?(&:staging) set_linear_issue_state(issue, staging_state) end def fetch_linear_issues(query) linear .query(query) .dig("data", "team", "issues", "nodes") .map do |issue| LinearIssue.new( id: issue.fetch("id"), number: issue.fetch("number"), title: issue.fetch("title"), state: issue.dig("state", "name"), pull_requests: issue.dig("integrationResources", "nodes").map do |pr| PullRequest.new( repo_name: pr.dig("pullRequest", "repoName"), number: pr.dig("pullRequest", "number"), ) end, ) end end def fetch_github_pr_merge_commit_oid(pull_request) # Fetch the PR's merge commit from GitHub github .query(%( query GitHub { repository(owner: "NoRedInk", name: "#{pull_request.repo_name}") { pullRequest(number: #{pull_request.number}) { mergeCommit { oid } } } } )) .dig("data", "repository", "pullRequest", "mergeCommit", "oid") end def commit_deploy_status(commit_oid) # Check in which remote branches this commit is included. branches = `git branch --contains #{commit_oid} --format='%(refname:short)' -a -r`.split(/\n+/) CommitDeployStatus.new( staging: branches.include?(DEPLOYED_TO_STAGING_GIT_TAG), production: branches.include?(DEPLOYED_TO_PRODUCTION_GIT_TAG), ) end def set_linear_issue_state(issue, state) update_linear_issue(issue.id, "stateId", %("#{state.id}")) puts "- Moved issue ##{issue.number} (#{issue.title}) from '#{issue.state}' to '#{state.name}'" end def update_linear_issue(issue_id, key, value) linear.query(%( mutation IssueUpdate { issueUpdate( id: "#{issue_id}", input: { #{key}: #{value} } ) { success } } )) end end LinearWorkflowChanger.new.do_it