Mês: julho 2009

Gestão de Configuração e Versões (SCM)

Objetivo

O objetivo deste artigo é explanar algumas opiniões práticas sobre gestão de configuração e versões, mais conhecido também como SCM (Software Configuration Management).

Como muitos de nós sabemos, essa é uma das “key process areas” do CMMi. Meu objetivo aqui não é descrever como faz para ganhar um desses certificados, e sim explicar como conseguir resultados práticos e operacionais, criando esses processos e práticas.

Controle de Versões x Gestão de Configuração e Versões

Muita gente acha que adotando um software de controle de versões, como o SourceSafe, Subversion, Team System, Clear Case ou Git automaticamente já tem um processo de gestão de configuração e versões.

No meu ponto de vista, esse processo é um pouco mais completo e complexo, visto que rastrear as alterações no código fonte é apenas parte dele.

A principal questão envolvida é que quando publicamos uma versão de um software, este é composto por um ou mais arquivos de código fonte (no caso de linguagens interpretadas), um ou mais binários (gerados à partir de um conjunto de arquivos fontes), bibliotecas necessárias para compilação e/ou execução, scripts de bancos de dados e uma série de outros artefatos, dependendo da aplicação.

Nesse caso começam os problemas… Exemplos:

  • A páginas asp pagina1.asp e pagina2.asp dependem de um include inc_default.asp. Se algum desenvolvedor mudou a assinatura de uma função no inc_default.asp e na pagina1.asp e esqueceu de mudar a pagina2.asp, a aplicação está quebrada. Ou melhor ainda. Se o desenvolvedor fez tudo direitinho, mas na hora de subir para o ambiente de produção, esqueceu de subir o include, quebrou o ambiente.
  • A aplicação foi toda criada pensando na versão 1 da biblioteca Spring.Net, de repente, o Spring.Net teve uma evolução para a versão 1.1 (com “breaking changes”). Todo o código fonte foi atualizado pensando nessa nova versão. No momento de subir a aplicação para produção, não atualizou-se a versão do Spring.Net. Quebrou o ambiente de produção.
  • A aplicação já está em produção e começa a ter evoluções. No meio do ciclo de vida das evoluções (teste/homologação, etc), encontrou-se um bug na aplicação. Como fazemos para corrigí-lo, já que temos funcionalidades instáveis já atualizadas no repositório de controle de versão? Voltamos a versão? Como saber qual é o conjunto exato dos arquivos e suas dependências que precisam ser voltados?
  • Adicionou-se um novo parâmetro numa stored procedure. O código foi atualizado considerando esse novo parâmetro, porém, na subida da versão, esqueceu-se de atualizar a procedure. Quebrou a aplicação em produção.
  • No caso de aplicações compiladas, com deploy feito pelo próprio desenvolvedor, vamos supor o seguinte cenário:
    • Temos um assembly DAL.dll (camada de acesso a dados).
    • O desenvolvedor “A” atualizou o arquivo Usuario.cs que está nesse assembly. Compilou sua versão e mandou subir para produção.
    • O desenvolvedor “B” atualizou o arquivo Perfil.cs que está nesse mesmo assembly. Compilou sua versão sem atualizar o arquivo Usuario.cs, mandou subir a versão para produção. Inconscientemente, desprezou as alterações do desenvolvedor “A”.

Em resumo, para se ter um processo de gestão de configuração e versão eficiente precisamos contemplar os seguintes pontos:

  • Versionamento de todos os artefatos que a aplicação depende para ser compilada e funcionar corretamente (código-fonte, scripts de base dados, documentação, etc.)
  • Estabelecer uma política de gestão de versões, em outras palavras, mecanismos para garantir a evolução do software e rastrear qual código fonte gerou qual binário.
  • Build automatizado para garantir integridade das compilações em relação ao repositório de controle de versões e qualidade no deploy da aplicação.

Controle de Versão – Versionamento dos Artefatos

Por incrível que pareça, existem empresas que ainda hoje não tem um software de controle de versões. A alternativa pra isso é uma pasta ou diretório compartilhado num servidor e várias pessoas acessando essa pasta.

Essa abordagem é somente para os corajosos. Vários problemas práticos podem surgir, como os exemplos abaixo:

  • Se dois usuários tentam trabalhar no mesmo arquivo, com certeza um vai acabar matando as alterações do outro.
  • Mesmo quando se tem uma política de backup bem definida (diário), se alguém altera um arquivo e não avisa ninguém, deixando o fonte instável, não se sabe quando foi a última alteração nesse arquivo em que a versão esteve consistente para restaurar um backup.
  • Não existe nenhuma rastreabilidade da alteração das versões.
  • Não existe nenhuma forma de garantir os binários em relação a esses fontes, devido a ausência de uma forma de “marcar” informações nos arquivos. É possível contornar isso gerando várias cópias do código-fonte, o que pode gerar um sério problema de armazenamento e gestão dessas cópias.

A alternativa a isso é procurar um software de controle de versões. Existem vários no mercado, para todos os gostos e bolsos. Desde gratuítos até suites mega-complexas, entre elas: Subversion (open-source), CVS (open-source), SourceSafe (pago), Team System (pago), Clear Case (pago), Perforce, Git, BitKeeper e uma série de outros. Provavelmente os links patrocinados do google (topo da página) devem estar mandando alguns links para ferramentas comerciais neste momento.

Cada uma delas tem seus benefícios e complicômetros. Particularmente gosto muito do Subversion que é open-source, mas nem todo mundo tem facilidade em aprender, utilizar e treinar equipes nessa ferramenta. Se tiver interesse nesse serviços, favor contatar-me através do meu e-mail ericlemes@gmail.com, ou mesmo deixar comentários no rodapé desta página.

Política de Gestão de Versões

Por que?

A resposta é simples: Garantir a evolução do software.

Todo mundo que já trabalhou na área de desenvolvimento de software sabe muito bem que para introduzir bugs num software, basta alterá-lo. É impossível estabilizar um software enquanto este não para de evoluir e ganhar novas funcionalidades.

Isso é verdade quando não se tem uma política de gestão de versões estabelecida. A idéia desse processo é justamente garantir “congelamentos” nas versões para permitir que as mesmas estabilizem-se enquanto evoluem em paralelo. Parece loucura, mas é isso mesmo. Sem esses processos é impossível garantir isso e nas explanações abaixo fica clara a razão.

Cenário tradicional – Uma linha de desenvolvimento

A maioria das equipes inconscientemente trabalha da forma descrita abaixo:

O gráfico acima ilustra a seguinte idéia:

  • Temos uma linha de tempo, que seria a evolução do software.
  • Com o decorrer do tempo, o software vem criando novas funcionalidades (linhas pretas cortando a linha do tempo). Para cada nova funcionalidade é feita uma alteração no código fonte (que pode ser em um ou mais arquivos).
  • Com o decorrer do tempo, encontram-se bugs no software (que podem ser de estabilização ou de produção). Esses bugs são corrigidos (linha vermelha cortando a linha do tempo).

Para entendermos os problemas gerados nesse cenário, vamos ilustrar o problema.

Começamos o desenvolvimento de um novo projeto, especificamos todas as funcionalidades e vamos começar a codificar a mesma. Por enquanto não tem nada em produção, nem em testes, então teremos um gráfico parecido com este:

Agora terminamos o primeiro ciclo de desenvolvimento. Baixa-se a última versão do repositório de controle de versão, compila a versão e sobe num servidor de testes.

Enquanto o software é testado, vamos desenvolvendo novas funcionalidades (afinal, desenvolvedor custa caro, né?). Nesse momento, vamos ter um gráfico parecido com este:

Um belo momento do tempo, considera-se que o software já está estável o suficiente para um release de produção. Novamente baixa-se a última versão do software, compila, sobe no servidor de produção e a vida continua.

Aí nesse momento começa o impasse, pois temos uma versão em produção, igual a uma versão de testes e uma única linha de desenvolvimento. Se começarmos a desenvolver a segunda fase do projeto, vamos criar novas alterações no controle de versões, conforme o gráfico:

Ainda não estabilizamos essa versão, que gera impacto profundo no software. Se nesse momento um bug em produção é encontrado, como fazemos para corrigí-lo e subir a nova versão?

  • Como saber qual o conjunto de versões corretas que precisamos voltar do controle de versão?
  • Como vamos garantir a subida dessa correção pontual sem deixar que as novas funcionalidades desenvolvidas quebrem as anteriores?

Em resumo, não tem uma forma “segura” de fazer isso. Precisamos da ajuda da sorte ou de muito tempo de análise dos desenvolvedores/gestores do projeto para garantir essa subida de versão.

Cenário tradicional 2 – Uma linha de desenvolvimento, com labels ou tags

Quase todo software de controle de versão possui funcionalidade de label (source safe) ou tag (svn/cvs). Essa funcionalidade permite “nomearmos” uma versão no controle de versão, para posteriormente podermos recuperá-la. Geralmente usa-se um label/tag em todos os arquivos do repositório de controle de versão, para podermos pegar o estado simultâneo de todos eles (no subversion, o conceito de revisão resolve parte desse problema).

Podemos simular o mesmo cenário acima, utilizando as labels, e chegaremos no mesmo problema:

Vamos supor que desenvolvemos o primeiro ciclo (3 novas funcionalidades) e estabilizamos (3 correções de bug), e subimos a versão para produção (label), chamando de “V1” essa versão, conforme gráfico abaixo:

Agora, começamos o novo ciclo de desenvolvimento (+ 3 novas funcionalidades), conforme gráfico abaixo:

Se nesse momento, encontramos um bug em produção, como fazemos?

Podemos facilmente recuperar a versão que está em produção, solicitando ao repositório a versão V1. Mas como vamos atualizar essa correção no controle de versões, caso este mesmo arquivo já tenha sido alterado por um novo desenvolvimento?

Temos duas alternativas:

  • Perder o novo desenvolvimento
  • Perder a rastreabilidade da correção (não haverá subida dessa correção para o controle de versão).

Em resumo, as duas alternativas são péssimas.

Branchs de Estabilização

A idéia dos branches de estabilização é prover um mecanismo para “congelar” a versão que está sendo estabilizada e simultaneamente permitir que a equipe de desenvolvimento continue investindo em novas funcionalidades.

Para entendermos essa política vamos criar um exemplo. Vamos supor que estamos desenvolvendo um novo aplicativo e 6 novas funcionalidades foram programadas e tiveram as alterações subidas para o repositório de controle de versão.

Agora, antes de iniciar a estabilização e testes dessas aplicações vamos criar um “branch” no controle de versão. O processo é similar a criar uma cópia, porém, os bons softwares de controle de versão guardam informações sobre baseada em que versão houve a ramificação, garantindo toda a rastreabilidade do processo.

Ficamos com nosso repositório no seguinte estado:

Quando houver necessidade de subir novas funcionalidades (não alterações decorrentes da estabilização das anteriores), essas alterações serão feitas na linha principal de desenvolvimento, da seguinte forma:

Assim, ficamos com o branch “Versão 1”, com a versão em estabilização “congelada”, ou seja, nos livramos daquele problema “não estabiliza nunca, porque não para de mexer”. Na linha principal de desenvolvimento fica a última versão, com mais features, porém, mais instável.

No caso de encontrarmos bugs na versão em estabilização, a correção deve ser feita nos dois branchs:

Essa “replicação”, ou “cópia” da correção pode ser assistida por ferramentas de “merge”. Óbviamente, na linha principal desenvolvimento, os arquivos podem ser diferentes. Somente as linhas alteradas devem ser inseridas na versão principal. Em alguns casos, pode ser que a correção precise ser feita novamente. Em geral o “merge” funciona muito bem.

Seguindo esse princípio novas correções são feitas até que consideramos a versão como “estável”. Nesse momento podemos criar um label somente dentro do branch, marcando esse estado para que saibamos “exatamente” o que vai sair no binário final (aquele estado em que estava a versão quando foi gerada).

No caso de novos bugs encontrados, após a geração da versão, uma nova versão pode ser gerada, repetindo o processo de subir as correções de bugs simultaneamente nos dois branchs e criando um novo label.

Posteriormente, as novas funcionalidades criadas na linha principal de desenvolvimento precisarão seguir o ciclo de vida, tendo a sua estabilização e posterior publicação. Para isso, seguimos o mesmo processo anterior, criando um novo branch para a nova versão.

Correções de bug na versão um, precisam ser replicadas em todos os branchs posteriores.

Correções de bug na versão 2, são replicadas em todos os branchs posteriores.

As novas funcionalidades entram somente na linha principal de desenvolvimento, que posteriormente terá um novo branch criado para estabilização da versão 3.

Quando a versão 2 tiver o seu release final, deve ser criado um label para garantir a rastreabilidade de quando essa versão foi gerada.

Como podemos observar essa abordagem garante toda rastreabilidade do processo, garantindo quando as ramificações foram criadas e principalmente garantindo o “congelamento” da versão.

A principal vantagem de estabelecer uma política desse tipo é fazer com que a equipe de desenvolvimento perca o medo de mexer estruturalmente na aplicação já que a versão de produção está lá guardada e sempre poderá ser recuperada ou modificada.

O problema gerado por essa abordagem é que o planejamento das versões precisa ser muito bem feito em conjunto com todas as áreas envolvidas, já que os releases acontecem sequencialmente e uma vez “congelada” a versão, novas funcionalidades não podem ser adicionadas.

O que acontece é que às vezes uma funcionalidade complexa entra no release e demora muito para ser homologada. A próxima versão fica também comprometida já que no momento do corte da próxima versão (no nosso exemplo, a versão 2), as funcionalidades instáveis da versão 1 já estão na linha principal de desenvolvimento.

Um problema muito comum desse tipo de política é para empresas que mantém uma única base de código para vários clientes e tem as versões com projetos associados a validações de clientes. Nesse caso, acontece a seguinte situação:

  • Durante a estabilização de uma versão 1, novas funcionalidades são adicionadas na linha principal de desenvolvimento, referentes a dois projetos, projeto A e projeto B.
  • Quando começamos a estabilizar a versão 2 esta já estará com as funcionalidades do projeto A + projeto B
  • Suponhamos que o Projeto A fez toda a lição de casa e está estável, porém, o projeto B não foi testado e validado pelo cliente.
  • Como a versão 2 contém os dois projetos, ela como um todo não poderá ser publicada em enquanto o projeto B não tiver finalizado.

Veremos alternativas para esses problemas nos dois cenários abaixo.

Branchs de Funcionalidades (Feature Branchs)

A idéia central de trabalhar com políticas de branchs de funcionalidades é manter a linha principal de desenvolvimento “estável”, e sempre que uma modificação ocorrer, ocorrer dentro de um branch, até que a mesma esteja estável e possa ser integrada à linha principal de desenvolvimento.

Na prática, podemos entender uma “modificação”, como um novo projeto, uma nova funcionalidade ou uma grande reestruturação na aplicação.

Essa política é comumente usada por pessoas que usam o Clear Case como software de controle de versões.

Na prática, funciona da seguinte forma: Vamos imaginar uma aplicação que teve seus commits iniciais e o primeiro release de sua versão (primeiras subidas de versão e primeiro label/tag marcando a versão):

Uma vez estabilizada a aplicação, sempre que uma nova funcionalidade for iniciada, começa-se um novo branch. No nosso exemplo, vamos supor que um novo projeto “Projeto 1” motivou essas alterações:

O branch do projeto 1 contém todas as atualizações até o momento de seu corte e as alterações referentes ao projeto. Tão logo estejam estabilizadas as novas funcionalidades, as mesmas são reintegradas (procedimento de merge) dentro da versão principal e uma nova versão é gerada.

Todo bom software de controle de versões (Ex.: Subversion) possui ferramenta de merge para simplificar esse processo de reintegração das alterações. Nosso gráfico fica da seguinte forma:

Sempre que bugs forem encontrados na versão estável, são corrigidos diretamente na linha principal de desenvolvimento:

O grande problema desta abordagem é que quanto mais tempo um branch de funcionalidade fica aberto, alterações podem ser reintegradas no branch principal antes dele. Quanto mais essa distância aumenta, mais difícil fica o merge do branch de funcionalidade para a linha principal, porque as versões se tornam cada vez mais diferentes e incompatíveis.

O exemplo abaixo, mostra esse cenário. O projeto 3 foi aberto e antes do término do projeto 2 (pois é maior e de complexidade maior) tanto correções aconteceram, quanto novas funcionalidades foram geradas (Projeto 3) e reintegradas na linha principal. A reintegração do Projeto 2 no branch, com certeza tende a ser mais dificultosa. Esse tipo de dificuldade deve ser observada, mas não inviabiliza a política. A qualquer momento pode-se fazer merges da linha principal de desenvolvimento para o branch de funcionalidades, para minimizar o impacto posterior.

Outro problema gerado por essa abordagem é quando mais de uma versão é gerida simultaneamente. Exemplo: Versões de testes, versões de produção, etc (como na política de branchs de estabilização). Olhando para o gráfico acima, vamos criar um cenário em que a versão gerada após o merge das alterações do Projeto 1 para a linha principal de desenvolvimento foi aplicada no ambiente de produção.

A equipe de desenvolvimento continuou trabalhando no projeto 2, fez algumas correções, começou e finalizou o projeto 3, realizando o merge para a linha principal de desenvolvimento. Se nesse momento encontrar-se um bug simples em produção, o bug seria corrigido na linha principal de desenvolvimento, e a única solução para atualizar essa versão seria subir todas as novas funcionalidades (projeto 3) para produção, junto com a correção do bug. Para atacar esses problemas, vamos pensar num modelo “híbrido”, discutido no próximo tópico.

Em contrapartida, uma grande vantagem dessa política é poder “escolher” quais projetos e quando serão integrados à linha principal de desenvolvimento. Para produtos que possuem “customizações” de clientes, é muito interessante, pois o casamento da integração com a versão principal está mais relacionado à decisão do cliente do que da empresa. Se um determinado projeto não evoluiu comercialmente, ele simplesmente é desprezado e não tem seu código fonte atualizado na linha principal de desenvolvimento.

Modelo Híbrido: Branchs de funcionalidades e estabilização.

A idéia desta política é juntar o melhor dos dois mundos. Conseguir a idéia de gestão do release como na versão de branchs de estabilização e a possibilidade de “escolher” o que vai ser integrado à versão principal, como nos feature branchs.

Para isso, vamos desenhar o cenário como fizemos nos exemplos anteriores.

Suponhamos um projeto que teve o seu release inicial:

Antes de disponibilizar a versão para estabilização, criamos um branch de estabilização (versão 1):

Nesse “estado” atual da aplicação, se a equipe de desenvolvimento começar a trabalhar com novas funcionalidades, deve criar um branch para este projeto, à partir da linha principal de desenvolvimento. Os bugs encontrados na versão 1 (em estabilização), são corrigidos na versão 1 e na linha principal de desenvolvimento:

Uma vez estabilizada a versão 1, é feito um label/tag na mesma e o release da mesma. Quando o projeto 1 precisar passar a ser estabilizado, ele é reintegrado à linha principal e um novo branch de estabilização (versão 2) é gerada:

Se nesse momento ocorrem bugs na versão 1, ele é corrigido e replicado na versão 1, versão 2 e linha principal de desenvolvimento.

O processo é repetido seguindo a mesma idéia. Quando novas funcionalidades precisar ser desenvolvidas, serão criados os branchs de projeto 2, projeto 3 e assim sucessivamente. Sempre que um dos branchs for integrado à linha principal, um novo branch de estabilização é gerado.

Deploy

O processo de “deploy”, que teve alguns dos seus problemas discutidos no 2o tópico deste artigo está diretamente relacionado a como os fontes são versionados e como a política é estabelecida.

Simplificar esse processo é mais fácil a partir do momento que existem “labels” de onde se possa recuperar o estado da versão como um todo.

Em outras palavras, sempre que quisermos fazer o deploy da aplicação, basta baixar todo o fonte baseado num label/tag, compilar ele de forma completa e enviar todos os scripts/binários/etc. Invés de fazer um build apenas parcial.

Essa abordagem resolve problemas como dois usuários alterando arquivos diferentes mas que são compilados num único binário. Num cenário em que o deploy é feito pelo próprio desenvolvedor, acontecem problemas como:

  • Usuário A baixou os fontes mais atualizados.
  • Usuário B baixou os fontes mais atualizados.
  • Usuário A alterou o Arquivo1.cs, do binário Lib.dll
  • Usuário A atualizou no ambiente de testes o binário Lib.dll
  • Usuário B alterou o Arquivo2.cs do binário Lib.dll
  • Usuário B atualizou no ambiente de testes o binário Lib.dll

No cenário acima, no último passo o Usuário B matou as alterações do usuário A, porque no binário dele não tinham as alterações do Arquivo1.cs. Nesse cenário, não tem como ter controle desse tipo de situação.

Utilizando a abordagem de labels e tags, fica o seguinte cenário:

  • Usuário A baixou os fontes mais atualizados
  • Usuário B baixou os fontes mais atualizados
  • Usuário A alterou o Arquivo1.cs do binário Lib.dll
  • Usuário A solicitou um Build.
  • O responsável pelos builds faz um label/tag no controle de versão, baixa a versão baseada nessa tag, realiza a compilação e sobe toda a versão para o ambiente de testes (ele não precisa se preocupar com qual binário foi alterado em função de cada fonte, simplesmente sobe toda a versão).
  • Usuário B alterou o Arquivo2.cs do binário Lib.dll
  • Usuário B solicitou um build
  • O responsável pelos builds faz um label/tag no controle de versão, baixa a versão baseada nessa tag e realiza a compilação.

No cenário acima, o processo de realizar o label/tag garante a consistência da versão como um todo, e para não haver preocupação na hora de decidir qual binário atualizar, é só atualizar sempre toda a versão.

Nada impede que esse processo de baixar os fontes, compilar toda a versão, empacotar e instalar seja automatizado, pelo contrário, as responsabilidades ficam bem definidas para o desenvolvedor (subir as alterações nos branchs corretos) e para o administrador do ambiente (aplicar as versões).

É possível rastrear todos os pontos do processo, inclusive qual binário está sendo executado em qual servidor e obter qualquer versão histórica deles em qualquer momento do ciclo de vida do software!

Conclusão

Como podemos observar, o processo de definir, criar uma cultura e institucionalizar uma política de gestão de versões é muito mais abrangente do que implementar um software de controle de versão.

Envolve uma abordagem mais estratégica em relação a qual é o objetivo da empresa em relação ao software desenvolvido (interno, customizável, produto, etc), ao ciclo de vida do mesmo e processos de outras áreas relacionadas ao desenvolvimento (testes, infra-estrutura, comercial, etc.).

Nos exemplos citados acima, podemos conhecer um pouco de cada uma das políticas e suas vantagens e desvantagens, de forma resumida. Espero que com essas informações seja possível sensibilizar e nortear equipes de desenvolvimento que ainda não tem uma política formal a adotar alguma.

Anúncios