Schlagwort-Archive: Teamcity

Anwender durch zielgerichtete Release Notes in die Produktentwicklung integrieren

In diesem Video zeige ich eine von vielen maßgeschneiderten Funktionen unseres Release Prozesses. Das Ziel dabei war es die Anwender noch mehr in die Produktentwicklung einzubinden.

Die langfristige Vision: Anwender bringen selbst die Anforderungen ein. Wie das genau gemeint ist und wie wir das umgesetzt haben, seht ihr im Video. Unter anderem kommen YouTrack und TeamCity zum Einsatz.

 

Über ein Rake-Skript, welches auf ein bereits existierendes YouTrack-Gem zurückgreift, sprechen wir die Rest-API an. Vielen Dank an Alexander Groß von GROSSWEBER für die Hilfe bei der Umsetzung unserer Vision.

require 'youtrack'
require 'ostruct'
module Build
class YouTrack
class View
attr_reader :issues, :by_department, :by_relevance
def initialize(issues)
@issues = issues
@by_department = department(issues)
@by_relevance = relevance(issues)
end
private
def department(issues)
create_view(issues, proc { |issue| issue.departments })
end
def relevance(issues)
create_view(issues, proc { |issue| issue.relevant_to })
end
def create_view(issues, group_by)
view = issues.inject({}) do |memo, issue|
group_by.call(issue).each do |rel|
previous_value = memo.fetch(rel, [])
new_value = previous_value << issue
memo[rel] = new_value.sort_by(&:issue_id)
end
memo
end
Hash[view.sort]
end
end
class << self
NOT_FOUND = proc { { 'value' => nil } }
def fixed_issues(from, to)
resource = client.issues
maybe_patch_unreleased_methods(resource)
query = query(from, to)
puts "Getting issue list from YouTrack using query: #{query}"
issues = resource.list(filter: query, max: 1000)
issues = force_list(issues)
transformed = transform(issues)
View.new(transformed)
end
private
def client
@client ||= Youtrack::Client.new do |c|
c.url = 'your-url'
c.login = 'your-user'
c.password = 'your-pw'
c.connect!
end
end
def maybe_patch_unreleased_methods(issues)
return if issues.respond_to?(:list)
warn 'Patching #list method'
# Get a list of issues for a search query.
#
# attributes
# filter string A query to search for issues.
# with string List of fields that should be included in the result.
# max integer Maximum number of issues to get. If not provided, only 10 issues will be returned by default.
# after integer A number of issues to skip before getting a list of issues.
#
def issues.list(attributes = {})
attributes[:max] ||= 10
get("issue?#{URI.encode_www_form(attributes)}")
end
end
def query(from, to)
"project: HECO_comWORK Fixed in build: #{from} .. #{to} Department: -KEINE #{excluded_subsystems}"
end
def excluded_subsystems
spec = ENV['RELEASE_NOTES_EXCLUDE_SUBSYSTEMS']
return unless spec
spec = spec.split(',')
return if spec.empty?
"Subsystem: #{spec.map { |s| "-{#{s}}" }.join(' ')}"
end
def force_list(issues)
issues = issues.to_hash['issueCompacts']['issue'] rescue []
([] << issues).flatten(1)
end
def transform(issues)
issues.map do |x|
field = x['field']
project = field.find { |f| f['name'] == 'projectShortName' }['value']
number = field.find { |f| f['name'] == 'numberInProject' }['value']
issue_id = "#{project}-#{number}"
subsystem = subsystem(field)
summary = field.find { |f| f['name'] == 'summary' }['value']
info = field.find(NOT_FOUND) { |f| f['name'] == 'Release Infotext' }['value']
departments = arrayify(field, 'Department')
relevant_to = arrayify(field, 'Relevant to')
OpenStruct.new(issue_id: issue_id,
subsystem: subsystem,
summary: summary,
info: info,
departments: departments,
relevant_to: relevant_to)
end
end
def subsystem(field)
value = field.find { |f| f['name'] == 'Subsystem' }['value']
return nil if value == 'No Subsystem'
value
end
def arrayify(field, key)
value = field.find(NOT_FOUND) { |f| f['name'] == key }['value']
return [] if value.nil?
([value]).flatten.sort
end
end
end
end
view raw youtrack.rb hosted with ❤ by GitHub

Wer weitere Einblicke in unseren Prozess bekommen möchte, der kann mir dazu einen Kommentar hinterlassen.

require 'youtrack'
require 'ostruct'
module Build
class YouTrack
class View
attr_reader :issues, :by_department, :by_relevance
def initialize(issues)
@issues = issues
@by_department = department(issues)
@by_relevance = relevance(issues)
end
private
def department(issues)
create_view(issues, proc { |issue| issue.departments })
end
def relevance(issues)
create_view(issues, proc { |issue| issue.relevant_to })
end
def create_view(issues, group_by)
view = issues.inject({}) do |memo, issue|
group_by.call(issue).each do |rel|
previous_value = memo.fetch(rel, [])
new_value = previous_value << issue
memo[rel] = new_value.sort_by(&:issue_id)
end
memo
end
Hash[view.sort]
end
end
class << self
NOT_FOUND = proc { { 'value' => nil } }
def fixed_issues(from, to)
resource = client.issues
maybe_patch_unreleased_methods(resource)
query = query(from, to)
puts "Getting issue list from YouTrack using query: #{query}"
issues = resource.list(filter: query, max: 1000)
issues = force_list(issues)
transformed = transform(issues)
View.new(transformed)
end
private
def client
@client ||= Youtrack::Client.new do |c|
c.url = 'your-url'
c.login = 'your-user'
c.password = 'your-pw'
c.connect!
end
end
def maybe_patch_unreleased_methods(issues)
return if issues.respond_to?(:list)
warn 'Patching #list method'
# Get a list of issues for a search query.
#
# attributes
# filter string A query to search for issues.
# with string List of fields that should be included in the result.
# max integer Maximum number of issues to get. If not provided, only 10 issues will be returned by default.
# after integer A number of issues to skip before getting a list of issues.
#
def issues.list(attributes = {})
attributes[:max] ||= 10
get("issue?#{URI.encode_www_form(attributes)}")
end
end
def query(from, to)
"project: HECO_comWORK Fixed in build: #{from} .. #{to} Department: -KEINE #{excluded_subsystems}"
end
def excluded_subsystems
spec = ENV['RELEASE_NOTES_EXCLUDE_SUBSYSTEMS']
return unless spec
spec = spec.split(',')
return if spec.empty?
"Subsystem: #{spec.map { |s| "-{#{s}}" }.join(' ')}"
end
def force_list(issues)
issues = issues.to_hash['issueCompacts']['issue'] rescue []
([] << issues).flatten(1)
end
def transform(issues)
issues.map do |x|
field = x['field']
project = field.find { |f| f['name'] == 'projectShortName' }['value']
number = field.find { |f| f['name'] == 'numberInProject' }['value']
issue_id = "#{project}#{number}"
subsystem = subsystem(field)
summary = field.find { |f| f['name'] == 'summary' }['value']
info = field.find(NOT_FOUND) { |f| f['name'] == 'Release Infotext' }['value']
departments = arrayify(field, 'Department')
relevant_to = arrayify(field, 'Relevant to')
OpenStruct.new(issue_id: issue_id,
subsystem: subsystem,
summary: summary,
info: info,
departments: departments,
relevant_to: relevant_to)
end
end
def subsystem(field)
value = field.find { |f| f['name'] == 'Subsystem' }['value']
return nil if value == 'No Subsystem'
value
end
def arrayify(field, key)
value = field.find(NOT_FOUND) { |f| f['name'] == key }['value']
return [] if value.nil?
([value]).flatten.sort
end
end
end
end

view raw
youtrack.rb
hosted with ❤ by GitHub

require 'youtrack'
require 'ostruct'
module Build
class YouTrack
class View
attr_reader :issues, :by_department, :by_relevance
def initialize(issues)
@issues = issues
@by_department = department(issues)
@by_relevance = relevance(issues)
end
private
def department(issues)
create_view(issues, proc { |issue| issue.departments })
end
def relevance(issues)
create_view(issues, proc { |issue| issue.relevant_to })
end
def create_view(issues, group_by)
view = issues.inject({}) do |memo, issue|
group_by.call(issue).each do |rel|
previous_value = memo.fetch(rel, [])
new_value = previous_value << issue
memo[rel] = new_value.sort_by(&:issue_id)
end
memo
end
Hash[view.sort]
end
end
class << self
NOT_FOUND = proc { { 'value' => nil } }
def fixed_issues(from, to)
resource = client.issues
maybe_patch_unreleased_methods(resource)
query = query(from, to)
puts "Getting issue list from YouTrack using query: #{query}"
issues = resource.list(filter: query, max: 1000)
issues = force_list(issues)
transformed = transform(issues)
View.new(transformed)
end
private
def client
@client ||= Youtrack::Client.new do |c|
c.url = 'your-url'
c.login = 'your-user'
c.password = 'your-pw'
c.connect!
end
end
def maybe_patch_unreleased_methods(issues)
return if issues.respond_to?(:list)
warn 'Patching #list method'
# Get a list of issues for a search query.
#
# attributes
# filter string A query to search for issues.
# with string List of fields that should be included in the result.
# max integer Maximum number of issues to get. If not provided, only 10 issues will be returned by default.
# after integer A number of issues to skip before getting a list of issues.
#
def issues.list(attributes = {})
attributes[:max] ||= 10
get("issue?#{URI.encode_www_form(attributes)}")
end
end
def query(from, to)
"project: HECO_comWORK Fixed in build: #{from} .. #{to} Department: -KEINE #{excluded_subsystems}"
end
def excluded_subsystems
spec = ENV['RELEASE_NOTES_EXCLUDE_SUBSYSTEMS']
return unless spec
spec = spec.split(',')
return if spec.empty?
"Subsystem: #{spec.map { |s| "-{#{s}}" }.join(' ')}"
end
def force_list(issues)
issues = issues.to_hash['issueCompacts']['issue'] rescue []
([] << issues).flatten(1)
end
def transform(issues)
issues.map do |x|
field = x['field']
project = field.find { |f| f['name'] == 'projectShortName' }['value']
number = field.find { |f| f['name'] == 'numberInProject' }['value']
issue_id = "#{project}#{number}"
subsystem = subsystem(field)
summary = field.find { |f| f['name'] == 'summary' }['value']
info = field.find(NOT_FOUND) { |f| f['name'] == 'Release Infotext' }['value']
departments = arrayify(field, 'Department')
relevant_to = arrayify(field, 'Relevant to')
OpenStruct.new(issue_id: issue_id,
subsystem: subsystem,
summary: summary,
info: info,
departments: departments,
relevant_to: relevant_to)
end
end
def subsystem(field)
value = field.find { |f| f['name'] == 'Subsystem' }['value']
return nil if value == 'No Subsystem'
value
end
def arrayify(field, key)
value = field.find(NOT_FOUND) { |f| f['name'] == key }['value']
return [] if value.nil?
([value]).flatten.sort
end
end
end
end

view raw
youtrack.rb
hosted with ❤ by GitHub

Build Script Interaktion mit TeamCity

Kürzlich standen wir vor der Aufgabe aus dem Build Skript, welches bei uns in Ruby geschrieben ist, Informationen an TeamCity, unseren Build Server, zu senden. Konkret wollten wir die interne Versionsnummer unseres Produkts in der Oberfläche anzeigen. Dazu muss lediglich im Skript eine Konsolenausgabe getätigt werden, die einem gewissen Format entspricht. In Ruby ist das der Befehl ‘puts’:

puts „##teamcity[buildNumber ‚#{build}‘]“

Dabei ist ‘build’ unsere interne Versionsnummer, die wir zuvor ermittelt haben. In einem Batch-Skript wäre dann der Echo-Befehl abzusetzen. Hier findet sich die passende Hilfe von JetBrains, welche weiteren Möglichkeiten es gibt.

Alexander Groß bietet hier ein fertiges Ruby Skript an, welches ihr bei euch einbinden könnt.

Aktuelle NuGet Packages in Build Skripten verwenden

Alex Groß von GROSSWEBER hat uns freundlicherweise eine Ruby Klasse geschrieben, welche es ermöglicht im Build Vorgang immer die aktuellste Version eines NuGet Package heranzuziehen.

Wer beispielsweise MSpec als UnitTesting Framework einsetzt und ein entsprechendes Skript in seinem Build Server verwendet, der hatte das Problem, dass das Skript angepasst werden musste, sobald man MSpec per NuGet aktualisierte. Das gilt natürlich für jegliches Package, auf welches in Skripten direkt referenziert wird.

Um die Problematik nun elegant zu lösen, sei exemplarisch unten folgendes Szenario aufgelistet:

 

Hier zeige ich den Ruby Code, welcher bei uns im Build Skript zur Ausführung der Unit Tests verwendet wird. In Zeile 3 wird die Lösung von Alex verwendet. Erster Parameter gibt den Namen des Package an, während zweiter Parameter besagt wo alle Packages im Repository liegen. Als Rückgabewert erhält man den Pfad zur aktuellsten Version. Im nächsten Schritt in Zeile 4 iteriere ich über unser Binaries Verzeichnis drüber. Dabei werden alle DLLs verarbeitet, die mit ‘comWORK’ beginnen. Als Ausnahme gebe ich alle Dateien an, die auf ‘resources’ enden. Das Ergebnis ist eine Liste von DLLs, welche MSpec dann prüft bzw. ausführt. Die Reports landen im Verzeichnis ‘Reports’ und liegen in Form von html Dateien zur Ansicht bereit.

   1: desc 'Unit Tests'

   2: task :unit do

   3:     mspec = NuGetLatest('Machine.Specifications', 'Source\_Solutions\packages')

   4:     FileList.new('Binaries/**/comWORK.*.dll').exclude('**/*.resources.dll').each do |f|

   5:         Mspec.run({

   6:             :tool => mspec + '/tools/mspec-clr4.exe',

   7:             :reportdirectory => 'Reports',

   8:             :assembly => f

   9:         })

  10:     end

  11: end

 

Hier das von Alex bereitgestellte Ruby File, welches immer das Verzeichnis der aktuellsten Package Version zurückliefert. Anpassungen sind keine nötig.

   1: class NuGetLatest

   2:   attr_reader :path

   3:  

   4:   def initialize(package_name, packages_dir = Dir.pwd)

   5:     @package_name = package_name

   6:  

   7:     raise "#{packages_dir} is not a directory" unless File.directory? packages_dir

   8:  

   9:     Dir.chdir packages_dir do

  10:       candidates = Dir["#{package_name}*"]

  11:       raise "No package '#{package_name}' was found in #{packages_dir}" unless candidates.any?

  12:  

  13:       latest = find_latest_version candidates

  14:       @path = File.join packages_dir, latest

  15:     end

  16:   end

  17:  

  18:   def to_s

  19:     @path

  20:   end

  21:  

  22:   def +(other)

  23:     to_s + other

  24:   end

  25:  

  26:   private

  27:   def find_latest_version(candidates)

  28:     return candidates.first if candidates.length == 1

  29:  

  30:     latest = candidates.map { |path|

  31:       version = path.sub /^#{@package_name}./, ''

  32:       version = parse_version version

  33:  

  34:       {

  35:         :version => version,

  36:         :path => path

  37:       }

  38:     }.max { |left, right| left[:version] <=> right[:version] }

  39:  

  40:     latest[:path]

  41:   end

  42:  

  43:   def parse_version(version_string)

  44:     major, minor, patch, revision, special = version_string.match(/^(\d+)\.(\d+)\.(\d+)\.?(\d+)?-?(.+)?$/).captures

  45:     SemVer.new(major.to_i, minor.to_i, patch.to_i, revision.to_i, special)

  46:   end

  47:  

  48:   # Borrowed from the SemVer gem and enhanced to support .NET's 4-digit versioning system.

  49:   class SemVer

  50:     attr_accessor :major, :minor, :patch, :revision, :special

  51:  

  52:     def initialize(major = 0, minor = 0, patch = 0, revision = 0, special = '')

  53:       major.kind_of? Integer or raise "invalid major: #{major}"

  54:       minor.kind_of? Integer or raise "invalid minor: #{minor}"

  55:       patch.kind_of? Integer or raise "invalid patch: #{patch}"

  56:       revision.kind_of? Integer or raise "invalid revision: #{revision}"

  57:  

  58:       unless special.nil? or special.empty?

  59:         special =~ /[A-Za-z][0-9A-Za-z-]+/ or raise "invalid special: #{special}"

  60:       end

  61:  

  62:       @major, @minor, @patch, @revision, @special = major, minor, patch, revision, special

  63:     end

  64:  

  65:     def <=>(other)

  66:       maj = @major.to_i <=> other.major.to_i

  67:       return maj unless maj == 0

  68:  

  69:       min = @minor.to_i <=> other.minor.to_i

  70:       return min unless min == 0

  71:  

  72:       pat = @patch.to_i <=> other.patch.to_i

  73:       return pat unless pat == 0

  74:  

  75:       rev = @revision.to_i <=> other.revision.to_i

  76:       return rev unless rev == 0

  77:  

  78:       spe = @special <=> other.special

  79:       return spe unless spe == 0

  80:  

  81:       0

  82:     end

  83:  

  84:     include Comparable

  85:   end

  86: end

  87:  

  88: module Conversions

  89:   def NuGetLatest(package_name, packages_dir)

  90:     NuGetLatest.new(package_name, packages_dir)

  91:   end

  92: end

  93:  

  94: include Conversions

 

Hier die MSpec Klasse zum Ausführen der Unit Tests. Vorsicht: Das Skript prüft eine Umgebungsvariable und ist dediziert auf uns zugeschnitten (Einsatz von Team City). Hier müsstet ihr also Anpassungen vornehmen.

//Update: Wie ich inzwischen erfahren habe, macht MSpec die Prüfung auf die von Team City gesetzte Umgebungsvariable inzwischen implizit!

   1: class Mspec

   2:   def self.run(attributes)

   3:     tool = attributes.fetch(:tool)

   4:     reportDirectory = attributes.fetch(:reportdirectory, '.').to_absolute

   5:     assembly = attributes.fetch(:assembly).to_absolute

   6:     

   7:     reportFile = assembly.name.ext('html').in(reportDirectory).to_absolute

   8:     FileUtils.mkdir_p reportFile.dirname

   9:     

  10:     mspec = tool.to_absolute

  11:     

  12:     Dir.chdir(assembly.dirname) do

  13:       sh "#{mspec.escape} #{'--teamcity ' if ENV['TEAMCITY_PROJECT_NAME']}--timeinfo --html #{reportFile.escape} #{assembly.escape}"

  14:     end

  15:   end

  16: end

Build Server Fehler mit licenses.licx

Ich hatte bereits früher über die Problematik gebloggt, dass Frameworks, welche Lizenzdaten beim kompilieren verarbeiten, auf Build Servern wie Team City Probleme machen können. DevExpress ist so ein Framework. Inzwischen haben wir in unserem Team eine Lösung gefunden: Die licenses.licx Dateien werden werden auf dem Build Server geleert (genauer: die vorhandenen Dateien werden durch leere ersetzt). Jetzt funktioniert alles problemlos, sodass Fehlermeldungen wie die unten der Vergangenheit angehören:

“My Project\licenses.licx(2): error LC0004: Exception occurred creating type ‚DevExpress.XtraTab.XtraTabControl, DevExpress.XtraEditors.v11.2, Version=11.2.5.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a‘ System.TypeInitializationException: Der Typeninitialisierer für "DevExpress.XtraTab.XtraTabControl" hat eine Ausnahme verursacht. —> System.Reflection.TargetInvocationException: Ein Aufrufziel hat einen Ausnahmefehler verursacht. —> System.IO.FileNotFoundException: Die Datei oder Assembly "EnvDTE, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" oder eine Abhängigkeit davon wurde nicht gefunden. Das System kann die angegebene Datei nicht finden.

Build Server Fehler mit licenses.licx

Wie viele andere sicherlich auch, verwenden wir ein Drittherstellerframework. Im vorliegenden Fall geht es um das kostenpflichtige DevExpress. Wir hatten kürzlich auf unserem Build Server mit Teamcity, auf welchem man DevExpress ebenfalls installieren muss (kurze Info: dafür benötigt man keine separate Lizenz), folgenden Compiler Fehler:

My Project\licenses.licx(2): error LC0004: Exception occurred creating type ‚DevExpress*‘ System.TypeInitializationException: Der Typeninitialisierer für "DevExpress.*" hat eine Ausnahme verursacht. —> System.Reflection.TargetInvocationException: Ein Aufrufziel hat einen Ausnahmefehler verursacht. —> System.IO.FileNotFoundException: Die Datei oder Assembly "EnvDTE, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" oder eine Abhängigkeit davon wurde nicht gefunden. Das System kann die angegebene Datei nicht finden. [*proj]

DevExpress* bedeutet, dass es sich um ein beliebiges Control handeln kann, welches in einem Projekt verwendet wird. Ein Fehler dieser Art kann theoretisch mit jedem beliebigen Framework, welches mit Lizenzen hantiert, auftreten. Bei DevExpress selbst habe ich einige Einträge dazu gefunden:

Es gibt im Netz dazu schon einige Einträge, unter anderem habe ich dazu einen recht aktuellen vom 25.10.11 gefunden, in welchem empfohlen wird, dass man diese Dateien aus dem Repository ausschließt. Weitere Links findet ihr am Ende.

Bei uns hat allerdings nur eine Lösung Wirkung gezeigt: Wir haben bei allen Entwicklern und auf allen Buildservern (inkl. aller Agents!) DevExpress entfernt und überall neu installiert. Da es in der Zwischenzeit ein Update gab, mussten wir mit dem DevExpress ProjectConverter alle Projekte aktualisieren lassen. Danach ging wieder alles. Ob die Neuinstallation schon ausgereicht hätte weiß ich nicht, da wir wie gesagt auch noch eine Konvertierung durchgeführt haben.

Restliche Quellen:

%d Bloggern gefällt das: