Mas encontramos uma saída - agora é impossível fazer commit no repositório sem testes. Pelo menos imperceptivelmente e com impunidade.
Sistema de teste
A primeira coisa de que precisamos é um sistema de teste. Já o descrevemos aqui. Lembre-se de que você precisa do mesmo código para ser executado no servidor Ci, e localmente, para que não haja dificuldade na manutenção. É desejável que o projeto seja capaz de definir vários parâmetros para testes comuns, ou ainda melhor - estendê-los com os seus próprios. Claro, o doce não saiu imediatamente.
Estágio um- você pode correr, mas dói. O que fazer com o código python ainda está claro, mas com todos os tipos de utilitários como CppCheck, Bloaty, optipng, nossas muletas internas, bicicletas - não. Para funcionar corretamente, precisamos de arquivos executáveis para todas as plataformas em que nossos colegas trabalham (mac, windows e linux). Neste estágio, todos os binários necessários estavam no repositório e o caminho relativo para a pasta de binários foi indicado nas configurações do sistema de teste.
<CppCheck bin_folder=”utils/cppcheck”>...</CppCheck>
Isso apresenta vários problemas:
- do lado do projeto, é necessário armazenar arquivos desnecessários no repositório, pois são necessários no computador de cada desenvolvedor. Naturalmente, o repositório é maior por causa disso.
- quando surge um problema, é difícil entender qual versão o projeto possui, se a estrutura necessária está na pasta.
- onde obter os binários necessários? Compile-se, baixe na Internet?
Estágio dois - colocamos as coisas em ordem nos utilitários. Mas e se você escrever todos os utilitários necessários e reuni-los em um repositório? A ideia é que no servidor já existam utilitários montados para todas as plataformas necessárias, que também são versionadas. Já usamos Nexus Sonatype, então fomos para o próximo departamento e combinamos os arquivos. O resultado é uma estrutura:
Para começar, você precisa de um script que conheça o endereço secreto onde estão os binários, possa baixá-los e também executar, dependendo da plataforma, com os parâmetros passados.
Omitindo os meandros da implementação
def get_tools_info(project_tools_xml, available_tools_xml): # Parse available tools at first and feel up dictionary root = etree.parse(available_tools_xml).getroot() tools = {} # Parse xml and find current installed version ... return tools def update_tool(tool_info: ToolInfo): if tool_info.current_version == tool_info.needed_version: return if tool_info.needed_version not in tool_info.versions: raise RuntimeError(f'Tool "{tool_info.tool_id}" has no version "{tool_info.needed_version}"') if os.path.isdir(tool_info.output_folder): shutil.rmtree(tool_info.output_folder) g_server_interface.download(tool_id=tool_info.tool_id, version=tool_info.needed_version, output_folder=tool_info.output_folder) def run_tool(tool_info: ToolInfo, tool_args): system_name = platform.system().lower() tool_bin = tool_info.exe_infos[system_name].executable full_path = os.path.join(tool_info.output_folder, tool_bin) command = [full_path] + tool_args try: print(f'Run tool: "{tool_info.tool_id}" with commands: "{" ".join(tool_args)}"') output = subprocess.check_output(command) print(output) except Exception as e: print(f'Fail with: {e}') return 1 return 0 def run(project_tools_xml, available_tools_xml, tool_id, tool_args): tools = get_tools_info(project_tools_xml=project_tools_xml, available_tools_xml=available_tools_xml) update_tool(tools[tool_id]) return run_tool(tool_info, tool_args)
No servidor, adicionamos um arquivo com a descrição dos utilitários. O endereço desse arquivo não foi alterado, então a primeira coisa que fazemos é ir até lá e ver o que temos em estoque. Omitindo sutilezas, esses são os nomes dos pacotes e o caminho para o arquivo executável dentro do pacote para cada plataforma.
xml "no servidor"
<?xml version='1.0' encoding='utf-8'?>
<Tools>
<CppCheck>
<windows executable="cppcheck.exe" />
<darwin executable="cppcheck" />
<linux executable="cppcheck" />
</CppCheck>
</Tools>
E no projeto, adicione um arquivo com a descrição do que você precisa.
projeto xml
, , , . .
:
, — .
- , , ? -, . — , . : git.
-, — bash-, git: pull push, , git-.
, :
, , , .git/hooks. — . , ( Windows Mac), . , .
, . , .
. , , git-bash Windows. FAQ.
. , . , FAQ. , .git/hooks . , :
, , :
— . .git/hooks, . . , .git/hooks , .
, , - . , -. — . , — . :
, : , . , .
<spoiler title=« :> : 32- ; , 64-; pip install , . - 32- — .
<?xml version='1.0' encoding='utf-8'?>
<Tools>
<CppCheck version="1.89" />
</Tools>
, , , . .
python -m utility_runner --available-source D:\Playrix\![habr]\gd_hooks\available_source.xml --project-tools D:\Playrix\![habr]\gd_hooks\project\project_tools.xml --run-tool CppCheck -- --version
:
- , ,
- , , . .
, — .
- ?
- , , ? -, . — , . : git.
-, — bash-, git: pull push, , git-.
, :
- pre-commit — . , .
- prepare-commit-msg — , . , rebase.
- commit-msg — . , . , .
, , , .git/hooks. — . , ( Windows Mac), . , .
, . , .
. , , git-bash Windows. FAQ.
: , , dns . , curl
[ .
. , . , FAQ. , .git/hooks . , :
git rev-parse
git rev-parse --git-path hooks
, , :
|
|
| Worktree |
|
| submodule |
|
— . .git/hooks, . . , .git/hooks , .
, , - . , -. — . , — . :
- pre-commit , . pre-commit-tmp
- commit-msg pre-commit pre-commit-tmp
, : , . , .
<spoiler title=« :> : 32- ; , 64-; pip install , . - 32- — .
Mas ainda assim, como lançar?
Primeiro, fizemos uma instrução de várias páginas sobre quais croissants são mais saborosos e quais python devem ser instalados. Mas será que nos lembramos dos designers de jogos e dos ovos mexidos? Sempre foi queimado: ou python da bitness errada ou 2.7 em vez de 3.7. E tudo isso também é multiplicado por duas plataformas onde os usuários trabalham: windows e mac. (Os usuários de Linux conosco ou gurus e configuram tudo sozinhos, batendo silenciosamente ao som de um pandeiro, ou passam o problema.)
Resolvemos o problema radicalmente - coletamos python da versão necessária e bitness. E à pergunta "como o colocamos e onde armazená-lo" eles responderam: Nexus! O único problema: não temos o python ainda para executar o script python que fizemos para executar os utilitários do Nexus.
E é aí que entra o bash! Ele não é tão assustador e até bom quando você se acostuma com ele. E funciona em qualquer lugar: no unix já está tudo bem, e no Windows é instalado junto com o git-bash (este é nosso único requisito para o sistema local). O algoritmo de instalação é muito simples:
- Baixe o arquivo python compilado para a plataforma necessária. A maneira mais fácil de fazer isso é por meio do curl - está quase em todos os lugares (até no Windows ).
Baixar pythonmkdir -p "$PYTHON_PRIMARY_DIR" curl "$PYTHON_NEXUS_URL" --output "$PYTHON_PRIMARY_DIR/ci_python.zip" --insecure || exit 1
- Descompacte-o e crie um ambiente virtual com um link para o binário baixado. Não repita os nossos erros: não se esqueça de acertar na versão virtualenv.
echo "Unzip python..." unzip "$PYTHON_PRIMARY_DIR/ci_python.zip" -d "$PYTHON_PRIMARY_DIR" > "unzip.log" rm -f "$PYTHON_PRIMARY_DIR/ci_python.zip" echo "Create virtual environment..." "$PYTHON_EXECUTABLE" -m pip install virtualenv==16.7.9 --disable-pip-version-check --no-warn-script-location
- Se você precisar de alguma biblioteca de lib / *, será necessário copiá-la você mesmo. virtualenv não pensa nisso.
- Instale todos os pacotes necessários. Aqui concordamos com os projetos que eles terão um arquivo ci / required.txt, que conterá todas as dependências no formato pip .
Instalando dependências
OUT_FILE="$VENV_DIR/pip_log.txt" "$PYTHON_VENV_EXECUTABLE" -m pip install -r "$REQUIRED_FILE" >> "$OUT_FILE" 2>&1 result=$? if [[ "$result" != "0" ]]; then var2=$(grep ERROR "$OUT_FILE") echo "$(tput setaf 3)" "$var2" "$(tput sgr 0)" echo -e "\e[1;31m" "Error while installing requirements. More details in: $OUT_FILE" "\e[0m" result=$ERR_PIP fi exit $result
Exemplo de Required.txt
pywin32==225;sys_platform == "win32" cryptography==3.0.0 google-api-python-client==1.7.11
Quando eles abordam um problema, geralmente anexam uma captura de tela do console onde os erros foram exibidos. Para tornar nosso trabalho mais fácil, não apenas armazenamos a saída da última execução de instalação do pip , mas também adicionamos cores à vida, exibindo os erros de cor do log diretamente para o console. Viva o grep!
Como fica
À primeira vista, pode parecer que não precisamos de um ambiente virtual. Afinal, já baixamos um binário separado, em um diretório separado. Mesmo se houver várias pastas onde nosso sistema está implantado, os binários ainda são diferentes. Mas! Virtualenv possui um script de ativação que o torna capaz de chamar o python como se estivesse no ambiente global. Isso isola a execução de scripts e facilita sua inicialização.
Imagine: você precisa executar um arquivo em lote a partir do qual um script Python é executado, a partir do qual outro script Python é executado. Este não é um exemplo fictício - é assim que os eventos pós-construção são executados durante a construção de um aplicativo. Sem o virtualenv, você teria que calcular os caminhos necessários em todos os lugares na hora, mas com ativarnós apenas usamos python em todos os lugares . Mais precisamente, vpython - adicionamos nosso próprio wrapper para facilitar a execução tanto do console quanto de scripts. No shell, verificamos se já estamos no ambiente ativado ou não, se estamos rodando no TeamCity (onde está nosso ambiente virtual), e ao mesmo tempo preparamos o ambiente.
vpython.cmd
set CUR_DIR=%~dp0 set "REPO_DIR=%CUR_DIR%\." rem VIRTUAL_ENV is the variable from activate.bat and is set automatically rem TEAMCITY - if we are running from agent we need no virtualenv activation if "%VIRTUAL_ENV%"=="" IF "%TEAMCITY%"=="" ( set RETURN=if_state goto prepare :if_state if %ERRORLEVEL% neq 0 ( echo [31m Error while prepare environment. Run ci\PrepareAll.cmd via command line [0m exit /b 1 ) call "%REPO_DIR%\.venv\Scripts\activate.bat" rem special variable to check if venv activated from this script set VENV_FROM_CURRENT=true ) rem Run simple python and forward args to it python %* SET result=%errorlevel% if "%VENV_FROM_CURRENT%"=="true" ( call "%REPO_DIR%\.venv\Scripts\deactivate.bat" set CI_VENV_RUN= set VENV_FROM_CURRENT= ) :eof exit /b %result% :prepare setlocal set RUN_FROM_SCRIPT=true call "%REPO_DIR%\ci\PrepareEnvironment.cmd" > NUL endlocal goto %RETURN%
Tanakan, ou não se esqueça de fazer testes
Resolvemos o problema de esquecimento para a execução de testes, mas até mesmo um script pode ser esquecido. Portanto, eles fizeram uma pílula para o esquecimento. Tem duas partes.
Quando nosso sistema é inicializado, ele modifica o comentário de confirmação e o marca como "aprovado". Como rótulo, decidimos não filosofar e adicionar [+] ou [-] no final do comentário ao commit.
No servidor que analisa as mensagens está rodando um script e, se não encontrar o desejado conjunto de caracteres, cria uma tarefa para o autor.Esta é a solução mais simples e elegante. Os caracteres não imprimíveis não são óbvios. Para executar ganchos de servidor, você precisa de um plano de tarifa diferente no GitHub, e ninguém vai comprar premium para um recurso. Percorrer a história dos commits, procurar um símbolo e definir uma tarefa é óbvio e não tão caro.
Sim, você pode colocar um símbolo com suas próprias canetas, mas tem certeza de que não quebrará a montagem no servidor? E se quebrar ... sim, o careca do Homescapes já está te seguindo.
Qual é o resultado final
É bastante difícil rastrear o número de erros que os ganchos encontraram - eles não chegam ao servidor. Existe apenas uma opinião subjetiva de que existem muito mais assembleias verdes. Porém, também há um lado negativo - o commit começou a demorar muito. Em alguns casos, pode levar até 10 minutos, mas essa é uma outra história sobre otimização.