Project Overdrive — ностальгическая ретроспектива на одну из самых перспективных игр отечественного геймдева

В России конца 1990-х и — особенно — начала 2000-х геймдев, как минимум производство одиночных тайтлов, был ещё на подъёме, и существовали разные амбициозные проекты, которые в том или ином виде доходили и до зарубежного пользователя. Как не вспомнить такие игры, как «Корсары», «ВанГеры», «Периметр», «Дальнобойщики» или «Космические Рейнджеры». Скорее всего, можно назвать ещё проекты, но уже с этими понятно развитие индустрии. Сейчас всё немного по-другому, да и рынок игр уже стал другим и продолжает меняться. Больше денег приносят онлайн и мобильные игры, а на полноценных законченных тайтлах зарабатывают компании, подчас только косвенно с ними связанные, например, Apple за счёт 30% комиссии с платежей.

Но сейчас разговор не о том, кто зарабатывает на играх. В своей книге «Кровь, пот и пиксели» Джейсон Шрайер писал о проекте 1313, масштабной игре по «Звёздным войнам», которая была закрыта «Диснеем». Вот и в России был свой проект 1313, закрытый компанией «Бука». Нет, это не «Капитан Блад», хотя она тоже подходит. Это довольно, по заявлениям разработчиков, мощный проект «Вне Закона», или на английском — Project Overdrive. Проект мог стать своеобразной революцией в играх. Интересно, что release-candidate (предварительную) версию в Интернет выложили спустя 11 лет после анонса. И выложил её не кто-то из разработчиков или тестировщиков, а критик, игровой журналист и эксперт по видеоиграм Святослав Торик.

Разработка игры началась ещё в далёком 1999 году. Люди, родившиеся в этот год, сейчас уже заканчивают вузы, уже имеют какой-никакой стаж работы или даже детей. На тот момент пресс-релиз у игры и описание сюжета были хоть и очень размытые, но с претензией на эпичность:

«Ты умён, технически грамотен и смертоносен: будешь ли ты проводить свои дни, будучи честным тружеником? Подумай ещё раз. Здесь достаточно высокооплачиваемой работы, и, учитывая твои связи в преступном мире, пришло время снова начать подниматься по криминальной лестнице, но помни: твоя репутация всегда будет опережать тебя.

Начни с малого — мудрое решение: можно стать курьером для местной мафии. Есть масса возможностей, если вы не против немного испачкать руки в крови или время от времени мыть их в слишком горячей воде. Стань телохранителем — или наёмным убийцей. Не подчиняйтесь приказам ни от кого, кроме Босса, или работайте только на себя. Накопи денег и езжай на машине — или просто укради её (не забывай остерегаться полиции – или просто пристрели их, если ты думаешь, что так будет проще)».

С другой стороны, текст выглядит больше как стандартное описание к любой средней проходной игре начала 2000-х. Но в совокупности с заявленными разработчиками механиками, это мог быть прорыв в игровой индустрии, в российской уж точно. Заявленные системные требования к игре: 1 GHz, 512 RAM, video 128 Mb, DirectX 9, HDD 600 MbВот. Ниже перечислены возможности, указанные разработчиками на англоязычном сайте «Буки» (веб-архивы всё помнят):

  • уникальный 3D-движок с портальной технологией обеспечивает постоянную частоту кадров при ходьбе или движении по улицам и внутри зданий;

  • никаких загрузочных экранов и, следовательно, никаких пауз при входе или выходе из зданий; 

  • всё подчиняется законам физики с беспрецедентной точностью, и это означает, что, как и в реальном мире, всё может быть разрушено: автомобили могут быть искорёжены, ветровые стекла разбиты, двери оторваны, люди искалечены, мебель разбита или сожжена; 

  • сложный искусственный интеллект: все персонажи имеют сложные и реалистичные модели поведения; 

  • автомобили и пешеходы подчиняются светофорам и знакам, разумно следуя по наилучшему маршруту, чтобы добраться до места назначения, и делая всё возможное, чтобы избежать столкновений; 

  • огромный город с различными районами, например, Чайнатаун; 

  • вы можете войти в любое здание в игре; 

  • большое разнообразие оружия и автомобилей; 

  • полная свобода действий; 

  • два многопользовательских режима: смертельный бой (deathmatch) или борьба за доминирование.

Как видно, планы у разработчиков были наполеоновские. Сейчас в 2022 году не все игры могут порадовать игрока заявленными возможностями, а тут их планировали выпустить в игре аж в 2001. Видимо, разработчики и издатели хотели завлечь побольше игроков. Например, единственная доступная версия игры есть только на английском языке и, скорее всего, делалась она с большим упором на западную аудиторию. Об этом говорят многие слова и словосочетания из описания игры, такие как «борьба за доминирование», «остерегайся полиции» (в РФ в 2001 году была ещё милиция), «20 долларов». Возможно, игра вообще изначально делалась на английском языке, а русскую озвучку добавили бы потом. Но об этом ничего пока не известно.

По найденным интервью от создателей игры и представителей пресс-службы складывается впечатление, что на момент интервью игра находилась на стадии начала разработки, а все наполеоновские планы представляли собой просто чек-лист для галочки. Правда, в версии, скачанной мной из Интернета, очень много багов и проблем. Но многие вещи реализованы достаточно оригинально, особенно, если делать поправку на 2000-2001 года. Видно, как разработчики создавали с нуля те механики, которые сейчас есть в любом, даже самом паршивом шутере от первого лица с открытым миром. Было желание сделать сравнение маркетинговых материалов и единственной существующей версии, потому что различные статьи от игроделов игру расписывают так, что создаётся впечатление чего-то огромного, революционного и эпохального. Но я не стал, так как проще глянуть игру без сравнения, да и прошло много лет, а думать, как могло бы быть, это просто не подкреплённые ничем фантазии.

К сожалению, неизвестно, почему проект был отменён. Конечно, это не «Капитан Блад», который был полностью доделан, а проблемы возникли чисто юридические: с правами, издателем, фирмой-разработчиком. В Project Overdrive достаточно проблем технического плана. Например, у меня игра запустилась только с третьего раза. Да, можно найти видео, где человек запускает игру, и у него сразу куча оружия, с помощью которого игрок демонстрирует, как разрушается мир. Я, возможно, что-то не так делал, а, возможно, мне не хватает каких-то консольных команд для игры. Но начинал я только с одним ножом. Далее во многих обзорах говорится о полной готовности игры. Насколько игра готова, я не знаю, но у меня она заработала, как я говорил, с третьего раза. Также мне пришлось её запускать через виртуальную машину. Кстати, в игре провалиться в текстуры или остаться без взаимодействия с игровым персонажем вполне реально. Много раз приходилось перезагружаться, когда возникали такие баги.

В этом обзоре я старался избегать сравнения с ещё одной очень похожей игрой. Беглого взгляда на заявленные в игре возможности и на реализацию достаточно, чтобы понять, что это GTA, а если точнее — GTA 3. А ведь правда, GTA 3 вышла почти в то же время, когда велась разработка Project Overdrive. Но даже если не сильно вдаваться во временные рамки, можно понять, что разработка обеих игр велась параллельно, и вряд ли кто-то у кого-то пытался украсть идеи. Причём если смотреть на обе игры, то похожи они только общей канвой. Реализация некоторых моментов в Project Overdrive вообще удивляет креативностью, ведь в 2001 году некоторые вещи в таких играх были либо реализованы по-другому, либо довольно плохо. Сейчас правда выглядит забавно, когда модели персонажей похожи: будто одну сторону сделали и потом отзеркалили. Что интересно, по городу действительно можно передвигаться, как в любых играх с открытым миром. И творить жесть — как в любой подобной игре. Правда, забавно: при сбивании машиной модельки персонажей превращаются в шар. Однако при нападении на того или иного бота можно получить в ответ вооруженного врага. Что касается разрушаемости — она правда поражает, для 2001 года очень много всего можно разрушить, особенно машины: они ломаются серьёзно и очень креативно. Но как я ни старался идти по сюжету, получалось у меня туго. Возможно, из-за проблем в английском, или мне чего-то не хватило. Ну и сравнивать с GTA 3 сложно, так как в ней всё-таки вид от третьего лица, в отличие от Project Overdrive.

Последнюю лучше сравнить с каким-нибудь Postal 2 — только без котов, глушителей и странных заданий. Правда, Postal 2 вышел через 2 года от предполагаемого выхода Project Overdrive. Возможно, очень возможно, криминальная составляющая в игре и правда должна была перетянуть на себя фанатов и игроков серии GTA.  В целом, идея криминальной игры про бандитов далеко не новая и уникальная идея. Тем более с открытым миром, который даёт карт-бланш на разного рода действия. Ведь клонов и похожих на GTA 3 игр выходило много. Даже Mafia, вышедшая почти в то же время, разрабатывалась параллельно, и вряд ли что-то заимствовала из GTA 3. Хотя сама серия могла вдохновить разработчиков взять к себе какие-то механики, что, на мой взгляд, всё же вряд ли возможно. Богатое криминальное прошлое России может вдохновить лучше, чем игры типа GTA. Да и боевики 1990-х вполне могут служить основой для создания собственной криминальной игры. Так что здесь могло иметь место и простое совпадение идей у нескольких команд-разработчиков.

Сейчас остаётся только гадать, почему отменили проект, уже находящийся на финальной стадии разработки. В наше время он получил бы зелёный свет, особенно если бы его выпустила компания по типу Electronic Arts, Blizzard или Bethesda. Но, видимо, объявление о революционной GTA 3, схожесть, на первый взгляд, сюжета и механик между двумя играми добавили бы проблем к выпуску. Истинную причину отмены узнать можно только у издателя и разработчиков игры.

Для игроманов-эстетов или игроманов-гуманов игра заслуживает внимания. К сожалению, я не помню, откуда её качал. Советую поискать на торрентах, но сразу скажу, что скачивал много разных вариантов, и только один оказался рабочим. Все остальные версии даже не запускались. Возможно, кто-то проходил или пройдет игру дальше меня, и для него она действительно окажется законченной. Но когда игра у меня вылетела на 8-ой раз, я подумал «хватит», тем более, что в ней не работали сохранения, либо опять я где-то не добавил каких-то действий для стабилизации игры.

Пока у меня на руках только довольно забагованная версия под названием release-candidate, скачанная с торрента, а также фантазии, какой бы фурор её мог ждать после выхода, и какой она бы была с исправлением всех багов и проблем. Но как говорит один ведущей одной передачи, тоже связанной с криминальной тематикой,: «Это уже совсем другая история».


ссылка на оригинал статьи https://habr.com/ru/post/685754/

Как мы строили безопасное web приложение на базе WIKI.JS

Цель статьи — поделиться опытом, болью и поныть немного о проблемах. Никоим образом не ставилось целью показать, что именно такой путь правильный.

Как все начиналось

  • Задача — создать help раздел на базе движка wikijs.

  • Версия wikijs, которую брали за основу – 2.5.268

  • Крайняя версия, которую ставили (на момент написания статьи) – 2.5.274

Особенности и условия

  • исходный код править нельзя. Во первых, у нас в команде нет фронтендера, во вторых, непонятно как это потом поддерживать, когда будут выходить новые версии wikijs с новыми фичами

  • должны быть устранены критические уязвимости (CVE)

  • на внешнем фронте должна быть закрыта админка

  • на сборочных раннерах нет интернета, ну почти нет — некоторые ресурсы открыты через прокси по согласованию с ИБ

Основной упор был нацелен на устранение уязвимостей, а там их оказалось не мало.

Сначала было желание оповестить разработчика обо всех имеющихся критических уязвимостях, но почитав Security Policy (https://github.com/requarks/wiki/security/policy) и скорость реакции на проблемы пользователей в Discussions, пришло понимание, что это плохой путь. Многие проблемы, озвученные в Discussions не решаются годами, хотя сам проект в целом активно развивается.

В Security Policy они хотят, чтобы по мимо известных CVE им присылали и примеры эксплоитов -:). Итак, было принято решение устранять уязвимости самим.

Для поиска существующих уязвимостей, использовали отчет Xray от JFrog, интегрированного с нашим docker registry.

Отчет XRAY

Для начала, надо понимать, что wikijs это гигантский комбайн с кучей всего, но весь функционал нам был не нужен, по этому, 1-й этап устранения уязвимостей – выкидывание не нужного. Каким образом удаляем не нужное – модифицируем package.json и пересобираем контейнер с приложением.

Что выкинули сразу

  1. sqlite3 – не нужно от слова совсем

  2. passport-microsoft – требуется для интеграции с azure (нам не нужно)

  3. mssql – в топку

  4. passport-azure-ad – azure нами не используется, удаляем

Следующий этап – апгрейд версий модулей, их зависимостей, а так же транзитивных зависимостей. Тут было много экспериментов. Если посмотрите файлик yarn.lock в исходном коде проекта – вам станет плохо. Там огромное количество транзитивных зависимостей. Чтобы было понимание о масштабах бедствия – этот текстовый файл имеет размер 6Mb!

Вторая большая боль – некоторые npm пакеты, используемые в данном проекте – давно не поддерживаются и имеют крайние обновления 3 – 5 лет назад, например: apollo-fetch — https://www.npmjs.com/package/apollo-fetch

А некоторые npm пакет и того пуще – deprecated, например graphql-tools версии 7.0.0 — https://www.npmjs.com/package/graphql-tools/v/7.0.0

Итак, после долгих экспериментов, что было сделано:
  1. highlight.js – заменяем на версию 10.4.1

  2. highlight.js присутствует также в зависимостях модуля diff2html. Повышаем версию diff2html до 3.4.14

  3. i18next — повышаем версию до 19.8.5

  4. js-yaml – повышаем версию до 3.14.1, также, это зависимость модуля i18next-node-fs-backend, разрешаем через resolutions: «i18next-node-fs-backend/js-yaml»: «3.14.1»

  5. sqlite-libs – непонятно из каких зависимостях они берутся и в каких слоях, но для верности удаляем  целиком модуль sqlite3, так как нам он не нужен.

  6. clean-css  — повышаем до версии 5.2.2.

  7. css-what – транзитивная зависимость модуля html-webpack-plugin – повышаем до версии 5.5.0 (css-what  -> css-select -> renderkid -> pretty-error -> html-webpack-plugin)

  8. xmldom – зависимость модуля passport-saml, повышаем версию последнего до 3.1.0

  9. lodash – зависимость модуля passport-azure-ad, удаляем последний целиком

  10. node-fetch – также одна из зависимостей passport-azure-ad

  11. node-forge – транзитивная зависимость passport-saml, решаем вопрос через resolutions: «passport-saml/**/node-forge»: «^1.0.0»

  12. nanoid – повышаем до версии 3.1.31

  13. ansi-regex 5.0.0 & 3.0.0 – залетают как зависимости пакетного менеджера npm вместе с образом nodejs 16.13.2. Поскольку, npm не учавствует в runtime на это можно закрыть глаза. Еще один вариант – сделать собственную сборку образа nodejs, в которую не ставить npm.

  14. ansi-regex – транзитивная зависимость eslint & cypress, решаем через механизм resolutions: «eslint/**/ansi-regex»: «^5.0.1», «cypress/**/ansi-regex»: «^5.0.1»

  15. underscore – зависимость пакетов express-brute & passport-cas, решаем вопрос через resolutions: «passport-cas/underscore»: «^1.8.3», «express-brute/underscore»: «^1.8.3»

  16. node-uuid – зависимость модуля passport-cas, разрешаем через resolutions: «passport-cas/node-uuid»: «1.4.8»

  17. markdown-it – повышаем версию до 12.3.2

  18. passport-oauth2 – зависимость модуля passport-microsoft, последний не обновляется уже 2 года и имеет уже крайнюю версию – удаляем целиком из package.json (нам не требуется)

  19. ansi-regex 2.1.1 – транзитивная зависимость mssql (ansi-regex -> strip-ansi -> gauge -> npmlog -> prebuild-install -> keytar -> @azure/identity -> tedious -> mssql), удаляем целиком из package.json

Что не удалось

Дополнительные, транзитивные зависимости node-fetch -> apollo-fetch & cross-fetch -> graphql-tools. Модули apollo-fetch & cross-fetch давно не обновлялись (5 лет назад последний раз и имеют крайнюю версию). Попытка изменить версию node-fetch для apollo-fetch, приводит к ошибке.

error: require() of ES Module /wiki/node_modules/apollo-fetch/node_modules/node-fetch/src/index.js  from /wiki/node_modules/apollo-fetch/node_modules/cross-fetch/dist/node.js not supported.  Instead change the require of index.js in /wiki/node_modules/apollo-fetch/node_modules/cross-fetch/dist/node.js  to a dynamic import() which is available in all CommonJS modules.

Модули ansi-regex версий 5.0.0 & 3.0.0 — залетают как зависимости пакетного менеджера npm вместе с образом nodejs 16.13.2. Поскольку, npm не участвует в runtime, на это можно закрыть глаза. Еще один вариант – сделать собственную сборку образа nodejs, в которую не ставить npm.

/usr/local/lib/node_modules/npm/node_modules/string-width/node_modules/ansi-regex /usr/local/lib/node_modules/npm/node_modules/ansi-regex /usr/local/lib/node_modules/npm/node_modules/cli-table3/node_modules/ansi-regex /usr/local/lib/node_modules/npm/node_modules/cli-columns/node_modules/ansi-regex

В модулях самого приложения, ansi-regex требуемой версии:

bash-5.1$ grep 'version' /wiki/node_modules/ansi-regex/package.json         "version": "5.0.1",

Ошибки в процессе сборки

warning Error running install script for optional dependency: "/wiki/node_modules/cpu-features: Command failed.   Exit code: 1   Command: node-gyp rebuild   Arguments:   Directory: /wiki/node_modules/cpu-features   Output:   gyp info it worked if it ends with ok   gyp info using node-gyp@8.3.0   gyp info using node@16.13.2 | linux | x64   gyp ERR! find Python 

Это ошибки сборки некоторых опциональных зависимостей и это можно игнорировать.

info This module is OPTIONAL, you can safely ignore this error

Интерпретатор python не установлен в контейнере намеренно, о чем указывает вышеописанная ошибка: «gyp ERR! find Python». В противном случае, будет осуществлена попытка сборки опциональных зависимостей из исходников, а это приведет к ошибке в пайплайне, так как будет попытка скачать исходники с разных ресурсов в интернете, а интернета на раннерах нет.

Изменения в package.json

diff2html 3.4.14

i18next 19.8.5

js-yaml 3.14.1

sqlite3 5.0.2

chalk 4.1.2

passport-microsoft 0.1.0

mssql 6.2.3 — remove

passport-saml 3.1.0

highlight.js 10.4.1

passport-azure-ad 4.3.1

html-webpack-plugin 5.5.0

cheerio 1.0.0-rc.10

nanoid 3.1.31

markdown-it 12.3.2

clean-css 5.2.2

resolutions:

    «i18next-node-fs-backend/js-yaml»: «3.14.1»

    «passport-cas/node-uuid»: «1.4.8»,

    «passport-cas/underscore»: «^1.8.3»,

    «express-brute/underscore»: «^1.8.3»,

    «eslint/**/ansi-regex»: «^5.0.1»,

    «cypress/**/ansi-regex»: «^5.0.1

    «passport-saml/**/node-forge»: «^1.0.0»

На какие поля еще стоит обратить внимание в package.json

«version»:

«2.5.274»,

«releaseDate»

«2022-01-29T18:45:51.000Z»,

«dev»

false,

Версию релиза и дату необходимо будет поддерживать самим. Если в поле dev будет true — увидите сверху красную полосу в UI, что приложение находится в «Development mode».

Чего делать нельзя!

Апгрейдить версию chalk до мажорной 5 и выше

Error [ERR_REQUIRE_ESM]: require() of ES Module /wiki/node_modules/chalk/source/index.js from /wiki/server/core/config.js not supported. Instead change the require of index.js in /wiki/server/core/config.js to a dynamic import() which is available in all CommonJS modules.     at Object.<anonymous> (/wiki/server/core/config.js:2:15)     at Object.<anonymous> (/wiki/server/index.js:17:14) {   code: 'ERR_REQUIRE_ESM' } 

Суть проблемы

Chalk 5 has changed to ESM. They provide a link to better understand what that means: Pure ESM. From chalk README: IMPORTANT: Chalk 5 is ESM. If you want to use Chalk with TypeScript or a build tool, you will probably want to use Chalk 4 for now.

Решение — понизить версию chalk до крайней мажорной ветки 4 — 4.1.2

Повышать версию js-yaml до 4-й мажорной и выше

>>> Unable to read configuration file! Did you create the config.yml file?      try {       appconfig = yaml.safeLoad(         cfgHelper.parseConfigValue(           fs.readFileSync(confPaths.config, 'utf8')         )       )

Решение без правки исходного кода — не повышать версию js-yaml до 4-й мажорной ветки. Крайняя версия в ветке 3 — 3.14.1

Решение с правкой исходных кодов — менять в исходниках appconfig = yaml.safeLoad на appconfig = yaml.Load

Повышать версию graphql-rate-limit-directive выше 1.3.0

2022-01-13T19:28:06.185Z [MASTER] info: Loading GraphQL Schema... 2022-01-13T19:28:06.189Z [MASTER] error: createRateLimitTypeDef is not a function const { createRateLimitTypeDef } = require('graphql-rate-limit-directive') // const { GraphQLUpload } = require('graphql-upload')

Решение — понизить версию graphql-rate-limit-directive до 1.2.1

Повышать версию graphql-tools выше 7.0.0

2022-01-13T20:02:47.802Z [MASTER] info: Loading GraphQL Schema... This package has been deprecated and now it only exports makeExecutableSchema. And it will no longer receive updates. We recommend you to migrate to scoped packages such as @graphql-tools/schema, @graphql-tools/utils and etc. Check out https://www.graphql-tools.com to learn what package you should use instead! 2022-01-13T20:02:48.574Z [MASTER] error: Class extends value undefined is not a constructor or null

Решение — не повышать версию graphql-tools выше 7.0.0

Апгрейдить версию apollo-server-express

2022-01-13T20:36:57.268Z [MASTER] error: You must `await server.start()` before  calling `server.applyMiddleware()`

Описание проблемы: This is a known bug with an open issue and a merged PR to fix it. For now, you can downgrade to apollo-server-express@^2 Пруфлинк

Решение — понижение версии apollo-server-express до 2.25.2

Одновременно ставить несколько модулей graphql разных версий, например через dependency & resolutions

2022-01-13T21:15:46.283Z [MASTER] error: Cannot use GraphQLObjectType "AnalyticsQuery" from another module or realm.  Ensure that there is only one instance of "graphql" in the node_modules directory. If different versions of "graphql" are the dependencies of other relied on modules, use "resolutions" to ensure only one version is installed.  https://yarnpkg.com/en/docs/selective-version-resolutions  Duplicate "graphql" modules cannot be used at the same time since different versions may have different capabilities and behavior. The data from one version used in the function from another could produce confusing and spurious results.

Решение — в package.json в dependency & resolutions должны быть одинаковые версии модуля graphql

Апгрейдить модуль graphql до 16-й мажорной ветки

2022-01-13T21:49:51.999Z [MASTER] error: graphql@16 dropped long-deprecated support  for positional arguments, please pass an object instead.

Решение — понижение версии модуля graphql до 15.3.0

Как строили пайплайн со сборочным конвейером

Примерный план

  • клонирование проекта с исходным кодом и переход на ветку с номером релиза

  • скачивание архива со сбилдженным приложением. Из данного архива берутся assets & server/views.

  • формирование .npmrc & .yarnrc для подключения remote-npm

  • скачивание пакетов локализации из проекта wiki-localization.git

Особенности реализации

  • Сборка осуществляется на раннерах гитлаба с подключением удаленного репозитория npm (remote-npm).

  • Этап build (yarn build) исключен из докерфайла, так как требует сборки некоторых модулей из исходников и соответственно, для этого нужен интернет.

  • Артифакты этапа build — assets, копируются из архива, официально сбилдженного приложения из github.

  • В контейнер с приложением, дополнительно копируются файлы локализации из проекта wiki-localization.git (https://github.com/Requarks/wiki-localization)

  • Файл конфига немного модифицировали (config.yml)

config.yml
port: 3000 bindIP: 0.0.0.0 db:   type: $(DB_TYPE)   host: '$(DB_HOST)'   port: $(DB_PORT)   user: '$(DB_USER)'   pass: '$(DB_PASS)'   db: $(DB_NAME)   storage: $(DB_FILEPATH)   ssl: $(DB_SSL) logLevel: $(LOGLEVEL) ha: $(HA_ACTIVE) offline: true

Описание процесса сборки

В качестве базового брали образ nodejs 16.13.2 alpine 3.15.

Стейдж build

  • Установка yarn

  • Копирование модифицированного package.json из данного проекта в рабочую директорию

  • Копирование .npmrc & .yarnrc в рабочую директорию

  • Сборка модулей и их зависимостей из списка dependencies

Стейдж release

  • Установка всех необходимых пакетов

  • Создание директорий

  • Копирование собранных модулей из стейджа build

  • Копирование папки servers из проекта с исходным кодом

  • Копирование assets & server/views из архива с официальной сборкой проекта

  • Копирование package.json

  • Копирование конфигурационного файла приложения config.yml

  • Копирование лицензионного соглашение (из офиц релиза)

  • Копирование файлов локализации

Dockerfile под катом
# ==================== # --- Build Assets --- # ==================== FROM artifactory.domain.ru/docker/node:16.13.2-alpine3.15 AS build RUN apk add yarn --no-cache  WORKDIR /wiki  COPY ./package.json ./package.json COPY ./.yarnrc ./.yarnrc COPY ./.npmrc ./.npmrc  RUN yarn cache clean RUN yarn --frozen-lock --production --non-interactive --verbose  # =============== # --- Release --- # =============== FROM artifactory.domain.ru/docker/node:16.13.2-alpine3.15 as release LABEL ru.domain.wikijs.title="Open source Wiki software based on Node.js" \       ru.domain.wikijs.description="Updated image for Wiki.js." \       ru.domain.wikijs.responsible="someone@domain.ru"  RUN apk add --no-cache -u \         bash \         curl \         git \         openssh \         gnupg && \     mkdir -p /wiki && \     mkdir -p /logs && \     mkdir -p /wiki/data/sideload && \     chown -R node:node /wiki /logs  WORKDIR /wiki  COPY --chown=node:node ./assets ./assets COPY --chown=node:node ./manifest.json ./assets/manifest.json COPY --chown=node:node --from=build /wiki/node_modules ./node_modules COPY --chown=node:node ./server ./server COPY --chown=node:node ./server/views ./server/views COPY --chown=node:node ./config.yml ./config.yml COPY --chown=node:node ./package.json ./package.json COPY --chown=node:node ./LICENSE ./LICENSE COPY --chown=node:node ./wiki-localization/locales.json ./data/sideload/locales.json COPY --chown=node:node ./wiki-localization/en.json ./data/sideload/en.json COPY --chown=node:node ./wiki-localization/ru.json ./data/sideload/ru.json  USER node  EXPOSE 3000  CMD ["node", "server"]

На этом процесс сборки контейнера с приложением закончен.

Второй этап — доработка напильником

Для того чтобы запретить любую возможность входа в административную панель приложения снаружи, было принято решение поднять 2 фронта. Примерно это выглядит так:

  • внутренний фронт с админкой — крутится в корпоративной сети (одна реплика)

  • внешний фронт без админки — торчит наружу (2 реплики)

  • оба фронта смотрят в один бэк — в одну БД (HA Cluster Patroni)

Как закрывали админку на внешнем фронте – закрытием локаций с использованием nginx на ingress контроллере кубера, где деплоили приложение. Пример:

nginx.ingress.kubernetes.io/server-snippet: |   location ~* "^/(login|_userav|h|u|e|logout|register|verify|login-reset|\.well-known|healthz)" {      return 301 https://$server_name;         }

Вторая часть задачи по закрытию возможности авторизации — выпиливание кнопки авторизации с панели управления, удалением куска js кода с использованием subs_filter nginx на том же ingress контроллере. Пример:

nginx.ingress.kubernetes.io/configuration-snippet: |       proxy_set_header Accept-Encoding "";       subs_filter "return\[[^\]]+?href:\"\/login[^\]]+?\]\)\][,]1\)\]" "return[]" ro;       subs_filter_types text/html text/javascript application/javascript;

Что еще делали – онлайн замену реальных пользователей, на обезличенного Administrator. Эта инфа отображается на каждой страничках wiki – Дата изменения и username. Тут так же — subs_filter от nginx.

subs_filter "author-name=\"(.*?)\"" "author-name=\"Administrator\"" ro; subs_filter "author-id=\"(\d+)\"" "author-id=\"0\"" ro;

Замена favicon, так же, через грязный хак. Ну нельзя штатными средствами это сделать.

subs_filter "(<meta name=\"msapplication-TileImage\" content=\"\/)_assets\/favicons\/mstile-150x150\.png(\">)" "$1new__favicon.ico$2" ro; subs_filter "<link(.+?)assets\/favicons\/(.+?)>" "" rg; 

Безопасность должна быть безопасной!

Наши апсеки узрели дырку в авторизации на локейшене /graphl. Целиком данный локейш выпилить нельзя — на нем реализован поиск контента. Пришлось применить lua скрипт по ограничению методов с просмотром тела запросов. Спасибо коллегам за помощь!

Магия nginx lua
location /graphql {             set $namespace      "{{ namespace }}";             set $ingress_name   "{{ app.name }}-ingress";             set $service_name   "{{ app.name }}";             set $service_port   "80";             set $location_path  "/graphql";             set $global_rate_limit_exceeding n;              limit_except POST {                 deny all;               }              rewrite_by_lua_block {               lua_ingress.rewrite({                 force_ssl_redirect = false,                 ssl_redirect = true,                 force_no_ssl_redirect = false,                 use_port_in_redirects = false,               global_throttle = { namespace = "", limit = 0, window_size = 0, key = { }, ignored_cidrs = { } },               })               balancer.rewrite()               plugins.run()                ngx.req.read_body()               local body = ngx.req.get_body_data()                if body then                   local m = ngx.re.match(body, "(search|pages|localization)")                   if m==nil then body="" end                    body = ngx.re.gsub(body, "authentication", "wrong")                   body = ngx.re.gsub(body, "mutation", "wrong")               end               ngx.req.set_body_data(body)             }              # be careful with `access_by_lua_block` and `satisfy any` directives as satisfy any             # will always succeed when there's `access_by_lua_block` that does not have any lua code doing `ngx.exit(ngx.DECLINED)`             # other authentication method such as basic auth or external auth useless - all requests will be allowed.             #access_by_lua_block {             #}              header_filter_by_lua_block {               lua_ingress.header()               plugins.run()             }              body_filter_by_lua_block {               plugins.run()             }              log_by_lua_block {               balancer.log()                monitor.call()                plugins.run()             }              port_in_redirect off;              set $balancer_ewma_score -1;             set $proxy_upstream_name "service-wikijs-80";             set $proxy_host          $proxy_upstream_name;             set $pass_access_scheme  $scheme;              set $pass_server_port    $server_port;              set $best_http_host      $http_host;             set $pass_port           $pass_server_port;              set $proxy_alternative_upstream_name "";              client_max_body_size                    1m;              proxy_set_header Host                   $best_http_host;              # Pass the extracted client certificate to the backend              # Allow websocket connections             proxy_set_header                        Upgrade           $http_upgrade;              proxy_set_header                        Connection        $connection_upgrade;              proxy_set_header X-Request-ID           $req_id;             proxy_set_header X-Real-IP              $remote_addr;              proxy_set_header X-Forwarded-For        $remote_addr;              proxy_set_header X-Forwarded-Host       $best_http_host;             proxy_set_header X-Forwarded-Port       $pass_port;             proxy_set_header X-Forwarded-Proto      $pass_access_scheme;              proxy_set_header X-Scheme               $pass_access_scheme;              # Pass the original X-Forwarded-For             proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;              # mitigate HTTPoxy Vulnerability             # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/             proxy_set_header Proxy                  "";              # Custom headers to proxied server              proxy_connect_timeout                   5s;             proxy_send_timeout                      60s;             proxy_read_timeout                      60s;              proxy_buffering                         off;             proxy_buffer_size                       4k;             proxy_buffers                           4 4k;              proxy_max_temp_file_size                1024m;              proxy_request_buffering                 on;             proxy_http_version                      1.1;              proxy_cookie_domain                     off;             proxy_cookie_path                       off;              # In case of errors try the next upstream server before returning an error             proxy_next_upstream                     error timeout;             proxy_next_upstream_timeout             0;             proxy_next_upstream_tries               3;              proxy_pass http://upstream_balancer;              proxy_redirect                          off;       } 

Интеграция с AD

Тут было несколько проблем

  • LDAP URL — Если указать ldaps://domain.ru:636, то возникала ошибка невозможности отрезолвить данный домен, при этом, сеть внутри контейнера работала нормально. Проблема в каком-то js модуле или методе. Ошибка воспроизводится 100% только в docker контейнере. Пришлось указывать тут IP PDC.

  • Проблема в понимании секции «Разрешить самостоятельную регистрацию/self-registration (https://docs.requarks.io/auth/ldap). Если не включать данную опцию и не прописывать «Назначить группу», то при авторизации нового пользователя получим «тыкву». Дело в том, что для полноценной работы, нужны локальные пользователи и локальная группа, которой будут даны определенные права. Если включить данную опцию, то при успешной LDAP авторизации, создадутся новые локальные пользователи, с правами группы, которая указана в данной настройке. Пользователи создадутся с именем = значению поля в AD, указанным в «Display Name Field Mapping». Либо необходимо заранее создать локальных пользователей, назначить им права, при этом имена пользователей должны совпадать со значением поля в AD указанного в «Display Name Field Mapping»

Известные проблемы

  1. Приложение может быть размещено только на выделенном домене, например wiki.domain.com и не может быть размещено в subpath, т.е. www.domain.com/wiki Пруф https://github.com/Requarks/wiki/discussions/4656

  2. Приложение пытается лезть в интернет, для скачки файлов локализации, по этому, если интернета нет на сервере, скачиваем пакеты локализации, подкладываем приложению (описано в dockerfile), а в конфиге config.yml прописываем offline: true

  3. Невозможно штатными средствами заменить favicon (применяли вышеуказанный хак). Обсуждение https://github.com/Requarks/wiki/discussions/3371, https://js.wiki/feedback/p/favicon

  4. Нельзя удалить/переименовать директории для медиафайлов (прям рука-лицо) https://feedback.js.wiki/wiki/p/delete-folders-in-the-image-file-manager

  5. Нельзя глобально изменить дефолтный timezone (тут тоже рука-лицо). Пруф https://feedback.js.wiki/wiki/p/select-date-format-and-time-zone-for-the-entire-site. Пока только такой грязный хак: ALTER TABLE users ALTER COLUMN timezone SET DEFAULT ‘Europe/Moscow ‘; По умолчанию пользователь получает timezone GMT-3 или что-то в этом духе.

  6. Настройка через текстовый конфиг невозможна. Например, настройка интеграции с LDAP, настройка локализации, создание групп, пользователей и т.д. Настройка предполагается только через UI. Автоматизация? Не, не слышали. Ну либо применять вливание настроек через sql скрипт.

  7. Некоторые вещи, например таблица стилей css, попадает в локальный файловый кэш на момент старта приложения. Этот кэш обновляется при изменении и сохранении контента страниц. Если несколько инстансов приложения (например несколько реплик в кубернетес для HA), то могут возникать такие коллизии — при логине в админку попадаешь на одно приложение в рамках сессии и после обновления контента, локальный кэш обновляется только у одного приложения. Далее если перейти на данный ресурс в браузере и понажимать F5 для обновления страницы, попадая на второй инстанс приложения, можно увидеть различия в оформлении, если оно менялось. Механизма управления локальным кэшем нет. По этому, сделали авторестарт внешних фронтов по расписанию (ночью).

Спрашивайте, может чего еще вспомню. А пока, пойду помою руки после nodejs.

Всем добра! -:)


ссылка на оригинал статьи https://habr.com/ru/post/684896/

Паттерны взаимодействия с ботами в Telegram: неочевидные практики на Python и баг в мессенджере

Я часто взаимодействую с ботами в Telegram. Чаще как пользователь, но создать собственного бота или потрогать чужого я не боюсь. При разработке собственного решения чувствуется, что бот не похож на GUI- или веб-приложение, но программисты тщательно превозмогают это чувство и делают так, как проще с точки зрения программирования.

В этой статье я расскажу про некоторые способы взаимодействия человека и бота в личных сообщениях и группах. Текст рассчитан на тех, кто только начинает изучать тему создания ботов, но, возможно, будет полезен и профессионалам.

Дисклеймер. Автор не является специалистом по UX. Изложенные тезисы не претендуют на звание лучших практик, а скорее показывают опыт автора, приобретенный на практике.

Шаблон


Статья практическая, поэтому предполагает фрагменты кода, которые наглядно продемонстрируют описанный подход. Для демонстрации я буду использовать свой основной язык программирования — Python. Итак, список требований:

  • Python 3.9.
  • Пакет python-telegram-bot версии 20.0a2 (python -m pip install python-telegram-bot==20.0a2).
  • Созданный бот в Telegram и токен доступа. Для создания обратитесь к BotFather.

Хотя примеры будут на конкретном языке программирования с использованием специфической библиотеки, идея не зависит от деталей реализации. Примеры можно перенести на другие языки программирования.

Фреймворк python-telegram-bot основан на обработчиках. Ядро получает обновления (Update) от Telegram Bot API и вызывает соответствующий обработчик из списка зарегистрированных. Если подходящего обработчика нет, то событие игнорируется.

Рассмотрим шаблон на примере простого echo-бота, который отвечает вашим же текстом.

import logging from telegram import Update from telegram.ext import *  # Логирование logging.basicConfig(     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) logger = logging.getLogger(__name__)  # Функция-обработчик async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:     await update.message.reply_text(update.message.text)  # Создание объекта Бот application = Application.builder().token("здесь ваш токен").build()  # Регистрация обработчика на текстовые сообщения, но не команды application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))  # Запуск бота application.run_polling()

Далее в примерах я буду приводить только функцию-обработчик и строку для регистрации обработчика.

После краткого введения приступим к обзору возможных взаимодействий с ботом. Бот — это программа, а программы должны исполнять команды. С них и начнем.

Команды


Команды в Telegram — это сообщения, начинающиеся со слэша (/). Примеры команд:

/start /subscribe@ExampleArticleBot

Первый вид команд используется в личных сообщениях. Имя бота добавляется в группах, чтобы явно указать, какому боту дана команда. Если в групповом чате написать команду без имени бота, то команда отправится всем ботам.

Команды — это хороший способ инициировать действие, так как список команд перечисляется в выпадающем меню чата с краткой справкой. При выборе команды сообщение отправляется незамедлительно. Это значит, что у команд не должно быть аргументов. Допустим, у нас бот в групповом чате с командой как на скриншоте выше, а команда принимает имя города через пробел. Таким образом, для получения погоды в Москве придется полностью напечатать следующий текст:

/weather@ExampleArticleBot Москва

Неудобно и отбивает всякое желание пользоваться ботом. Единственная команда, которая может получать аргументы, — это /start, и только при переходе по ссылке, которая выглядит следующим образом:

https://t.me/<имя_бота>?start=<строка>

В этом случае у пользователя появится кнопка START, даже если пользователь уже активировал бота. При нажатии кнопки в чат отправится сообщение /start, но бот получит сообщение /start <строка>. Создадим обработчик аргументов команды start:

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:     if update.message.text == "/start":         await update.message.reply_text("Start without arguments")         return      # Удаляем /start     arg = update.message.text[7:]     await update.message.reply_text(arg)  # Регистрация обработчика application.add_handler(CommandHandler("start", start))

Подобный подход позволяет боту задать первоначальный контекст обращения или помочь вести аналитику переходов, почти как UTM-метки.

Существенный минус команд проявляется в групповых чатах. Команды в отправленных сообщениях кликабельны, поэтому одна неосторожная команда может привести к лавине сообщений. Возможное решение — использование текста.

Текстовые сообщения


Специфичные команды можно представить в виде кодовых слов. Например, вместо /start запрограммировать бота реагировать на «Поехали!». Это отличное решение для ботов, которые в группах реагируют только на сообщения администраторов. Но есть в ложке меда бочка дегтя:

  • Документация по командам бота должна распространяться отдельно.
  • Программист должен учесть возможную вариативность сообщений.
  • Бот должен иметь модификатор «имеет доступ к сообщениям», что может снизить доверие к боту.

Неожиданный сюжетный поворот: бот способен получать ответы на свои сообщения даже если в группе он «не имеет доступ к сообщениям». В python-telegram-bot для этого есть абстракция ConversationHandler.

Получается хорошая комбинация из команды, которая инициирует запрос, и текстовых аргументов. При этом бот не получает доступ ко всем сообщениям в чате, что несколько повышает безопасность.

# Точка входа в диалог async def weather(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:     await update.message.reply_text("В каком городе хотите посмотреть погоду?")     return 1   # Обработка ответа async def show_weather(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:     city = update.message.text     await update.message.reply_text(         f"Вы хотите посмотреть погоду в городе {city}.\n"         f"\n"         f"Но я не умею показывать погоду, извините :("     )     return ConversationHandler.END  # Задаем точки входа и ветви диалога handler = ConversationHandler(     entry_points=[CommandHandler("weather", weather)],     states={         1: [MessageHandler(filters.TEXT & ~filters.COMMAND, show_weather)]     },     fallbacks=[] ) application.add_handler(handler)

В некоторых случаях бот предлагает несколько вариантов на выбор вместо свободного ввода. В этом случае уместно использовать кнопки.

Если вас интересует тема Telegram-ботов, посмотрите, что у нас есть еще на эту тему:

Как сделать бота для Telegram на облачных функциях
Как сгенерировать стикеры из сообщений в Telegram

Кнопки

ReplyKeyboard в действии

В Telegram существует два вида кнопок, которые могут быть созданы сообщением от бота. Первый вид — ReplyKeyboard, заменяющий клавиатуру на сенсорных устройствах. Нажатие на эту кнопку отправляет в чат текст кнопки.

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:     keyboard = [         ["Кнопка 1", "Кнопка 2"],         ["Большая привлекательная кнопка кнопка"]     ]     await update.message.reply_text(         "Какую кнопку будем нажимать?",         reply_markup=ReplyKeyboardMarkup(             keyboard,             one_time_keyboard=False,             input_field_placeholder="Ваш выбор?"         )     )  application.add_handler(CommandHandler("start", start))

Такие кнопки можно использовать в том числе в групповых чатах. Современные клиенты Telegram автоматически отправляют сообщение в виде ответа на сообщение бота. При локализации бота придется менять обработчики для поддержки сообщений на разных языках. Также кнопки этой клавиатуры отвечают на последнее сообщение, которое создало клавиатуру.

Если хочется разные действия для нескольких сообщений одновременно, то на помощь приходит InlineKeyboard — клавиатура под сообщением.

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:     keyboard = [         [             InlineKeyboardButton("Кнопка 1", callback_data="button-1"),             InlineKeyboardButton("Кнопка 2", callback_data="button-2")         ],         [InlineKeyboardButton("Большая привлекательная кнопка кнопка", url="https://habr.com/")]     ]     await update.message.reply_text(         "Какую кнопку будем нажимать?",         reply_markup=InlineKeyboardMarkup(keyboard)     )   async def weather(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:     keyboard = [         [             InlineKeyboardButton("Санкт-Петербург", callback_data="LED"),             InlineKeyboardButton("Москва", callback_data="SVO"),             InlineKeyboardButton("Иркутск", callback_data="IKT")         ]     ]     await update.message.reply_text(         "Где хотите посмотреть погоду?",         reply_markup=InlineKeyboardMarkup(keyboard)     )  application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("weather", weather))

Кнопки встроенной клавиатуры разнообразны и могут содержать один из следующих элементов:

  • callback_data — строка для специальных обработчиков, рассмотрим подробнее позже.
  • url — ссылка на любой ресурс. Кнопка со ссылкой отмечается стрелкой в верхнем правом углу.
  • inline_query — запускает inline-режим в указанном чате с текущим ботом. Наиболее известный бот с inline-режимом — gif.
  • callback_game — ссылка на игру.
  • web_app — ссылка на WebApp-приложение, доступно только в личных сообщениях.
  • login_url — ссылка на аутентификацию в сервисе через Telegram.
  • pay — ссылка на оплату счета через кошелек в Telegram.

Кнопки с действием, если это не callback_data, открывает новое окно с указанным действием. Если у сообщения одна кнопка, то она будет продублирована в закреплении, что может быть полезно в групповых чатах.

Максимальное количество кнопок под сообщением — 100, вне зависимости от компоновки. При превышении этого числа Telegram не выводит ошибку, но «лишние» кнопки не отображает.

Вернемся к обработке действия с callback_data. Нажатие на кнопку генерирует событие callback_query.

Обработка нажатия кнопки

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:     keyboard = [         [InlineKeyboardButton("❤️", callback_data="like-trex")]     ]     await update.message.reply_text(         "Нажми лайк, чтобы поддержать Тирекса!",         reply_markup=InlineKeyboardMarkup(keyboard)     )   async def query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:     # Убираем кнопки     await update.callback_query.message.edit_reply_markup(None)      # Отмечаем, что мы обработали событие и выводим текст     text = f"Спасибо, {update.callback_query.from_user.full_name}, что поддержал Тирекса!"     await update.callback_query.answer(text, show_alert=True)   application.add_handler(CommandHandler("start", start)) application.add_handler(CallbackQueryHandler(query))

При нажатии на кнопку в верхнем правом углу появляется пиктограмма часов. Это значит, что событие передано боту. Обработчик callback_query может ответить разными способами:

  • Обработать событие «тихо». На кнопке исчезнет пиктограмма часов.
  • Ответить всплывающим текстом. Этот способ варьируется в зависимости от клиента, но идея заключается в появлении текста поверх чата на короткий промежуток времени.
  • Ответить всплывающим окном. Текст отображается всплывающим окном с кнопкой «ОК».
  • Открыть чат с пользователем по ссылке или запустить игру в Telegram по ссылке.

Не пытайтесь сделать в обработчике много действий с сообщением подряд. В одном из своих проектов я выяснил, что открепление сообщения и удаление кнопок под сообщением в одно время «роняет» Telegram Desktop на Windows и Linux. Я оставил сообщение об ошибке для разработчиков Telegram.

В одной из следующих статей я расскажу о том, как сделал бота для массового заказа шавермы в Selectel. А пока вы можете подписать на бота компании — он рассказывает о предстоящих мероприятиях компании.


ссылка на оригинал статьи https://habr.com/ru/company/selectel/blog/685206/

Git — несколько решений Visual Studio в одном репозитории

Из VS2022 невозможно создать один репозиторий Git для хранения нескольких решений VS, поэтому для достижения этой цели мы использовали SourceTree. В статье представлены технические подробности этого процесса.

У меня появилась задача — я хотел опубликовать на GitHub исходный код из одной своей статьи, чтобы пользователи могли его скачивать. Проблема заключалась в том, что исходный код для одной статьи состоял из четырёх решений Visual Studio на C#. Я не хотел мерджить решения в одно, к тому же это было бы глупо, ведь эти решения VS содержали проект с одинаковым названием, который являлся эволюцией кода, объясняемого мной в статье. Я хотел, чтобы пользователь смог получить весь исходный код из статьи в одном клоне. Структура папок выглядит так:

Я открыл VS2022 и оказался в тупике. В VS2022 один репозиторий Git должен соответствовать одному решению VS. А у меня было несколько решений VS на C#, которые мне нужно было упаковать в один репозиторий. И что теперь делать?

1. Проверяем технологию: Git Submodule или Git Subtree

Я подумал, что мне поможет часть технологии Git, например, Submodule или Subtree. Однако после их тщательного изучения оказалось, что они не предназначены для этой задачи. Я не хотел делиться кодом библиотеки и получать всё с GitHub за один раз. Мне не удалось найти, как в Git создать «рабочее пространство», содержащее несколько репозиториев и обрабатываемое как один с точки зрения «git clone».

2. Решение — создание единого репозитория в SourceTree

Я решил, что лучше всего будет создать репозиторий Git в папке более высокого уровня, содержащей все четыре решения. Таким образом мне удастся хранить четыре решения в одном репозитории Git. Я сразу понял, что у меня будут ветки для репозитория, а не для каждого решения VS.

▍ 2.1. Создание репозитория

Я изучил опции Git в VS2022 и понял, что они ограничены, поэтому мне придётся использовать другой инструмент Git. Для создания репозитория папки высокого уровня я решил воспользоваться Sourcetree.

Пока коммиты выполнять нельзя! Сначала нам нужно настроить файл .gitignore. Если не настроить .gitignore, то в репозиторий будут сохраняться все файлы проекта, в том числе двоичные файлы и промежуточные файлы сборок, а нам этого не нужно.

▍ 2.2. Настраиваем .gitignore

Для эмуляции работы VS2022 я скопировал в корень моего репозитория два файла, сгенерированных VS2022: файлы .gitignore и .gitattributes, которые были созданы в другом проекте на C#, а затем закоммитил их.

Мы делаем это вручную, поскольку не используем VS2022 для создания репозитория Git; в противном случае VS2022 автоматически создал бы файл .gitignore, соответствующий выбранному типу проекта. Так как мы взяли на себя ответственность по ручному созданию репозитория в SourceTree, именно мы должны создать правильный файл .gitignore.

▍ 2.3. Коммиты файлов решений VS на C#

Затем я закоммитил все файлы решений Visual Studio в репозиторий. Так как .gitignore настроен, коммит будет игнорировать двоичные файлы и тому подобное.

▍ 2.4. Создание репозитория GitHub

Далее я создал удалённый аккаунт репозитория на GitHub и запушил файлы.

Можно просмотреть репозиторий на удалённой стороне и убедиться, что в него не выполнен коммит двоичных файлов или других файлов проекта. Если это произошло, значит, что-то не так с файлом .gitignore.

▍ 2.5. Мерджинг веток, которые невозможно смерджить

Проблема, которая возникла у меня сейчас, но, я уверен, возникнет многократно у меня и других людей, в том, что в конечном итоге у меня получились две ветки: main и master. Ветку main по умолчанию создаёт GitHub, в ней находится файл readme. Ветку master по умолчанию создаёт SourceTree, и в ней находится мой код (четыре решения VS).

Нам не нужны здесь две ветки, поэтому давайте смерджим их и избавимся от одной. Сохраним стандартную ветку main GitHub и удалим master.

Но при попытке слияния этих двух веток возникает проблема:

Это приводит к ошибке: fatal: refusing to merge unrelated histories.

Поскольку наши две ветки не имеют общего коммита-предка, SourceTree считает, что мы делаем что-то не так. Но мы знаем, что правы, и хотим заставить его это сделать.

В [1] написано, что эту проблему нельзя решить через GUI SourceTree. Значит, нам нужно выполнить команды командной строки Git в GitBash. Объяснение проблемы и инструкции приведены в [2].

Нужно выполнить git merge <branch-name> --allow-unrelated-histories.

Итак, можно открыть GitBash из SourceTree (кнопка Terminal), и исполнить команду. Вам будет предложено ввести комментарий к коммиту в отдельном текстовом редакторе. Затем продолжится мерджинг.

И вот как будут выглядеть слитые ветки:

Далее запушим изменения в GitHub и проверим, что в ветке main теперь находятся все наши файлы:

▍ 2.6. Удаление ненужной ветки

Теперь можно удалить ветку master локально в SourceTree и удалённо в GitHub. Вот как всё выглядит. История веток сохранилась, но в ней нет ветки master:

3. Проверка решения в VS2022

Далее нужно проверить, как всё выглядит на стороне VS2022. Сначала получим URL клонирования:

Затем клонируем репозиторий в VS2022.

И вот что мы получили: все четыре решения одновременно.

Нажав на одно из решений, мы перейдём к нему:

Нажав на кнопку выше, мы можем перейти к просмотру всех четырёх решений.

Именно этого мы и хотели. Единственная проблема заключается в том, что все четыре решения находятся в одной ветке — ветке main. Вероятно, лучше всего было бы создать по отдельной ветке для каждого решения и проверять изменения в коде там.

4. Заключение

Большинство моих проектов ведётся в TFS, поэтому я был поражён, насколько просто победить VS2022 простой идеей о помещении нескольких решений в один репозиторий Git. Без внешнего инструмента наподобие SourceTree или командной строки GitBash было бы невозможно это сделать.

Насчёт Git я подумал следующее: разве это не опенсорсный проект? Нельзя ли дополнить его концепцией «рабочего пространства», которое сможет содержать несколько репозиториев Git? Это позволило бы избежать неприятной ситуации, в которой все четыре решения находятся в одной ветке. Если у вас есть время изучить этот вопрос, то рекомендую посмотреть ссылки на сообщество Git [3] и [4].

5. Ссылки

Конкурс статей от RUVDS.COM. Три денежные номинации. Главный приз — 100 000 рублей.


ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/685300/

Вот как мы поняли, что нам нужно больше стажеров

Всем привет! Меня зовут Алексей Половинкин, я руковожу отделом Python в AGIMA. Последние пару лет мы набираем в команду больше стажеров. Это был наш эксперимент — хотели посмотреть, что из этого выйдет. Результат впечатлил: мы поняли, что нам нравится с ними работать. В этой статье я попробую разрушить несколько стереотипов о стажировках и объяснить, в чем плюсы работы с неопытными специалистами.

Предпосылки

Наша компания на рынке уже больше 15 лет. Мы занимаемся аутсорс-разработкой для крупных бизнесов и за это время реализовали огромное количество сложных проектов. Вместе с тем мы плотно работаем с подрядчиками, постоянно ищем людей как к себе в штат, так и в штат клиентов. Для примера, за прошлый год я провел больше 100 собеседований, в этом году уже 50.  

Собеседований очень много: из 10 кандидатов мы берем 1. Большая часть из них не проходит по навыкам. Встречались разработчики, у которых 8–10 лет опыта, но которые практически не разбираются во внутренностях фреймворков, никогда не занимались оптимизацией ORM, плохо знакомы с SQL и т. п. Кандидаты просят высокую зарплату, но не дотягивают даже до Middle-грейда нашей компании.

И это, честно говоря, удручает. Зачастую поиски адекватного разработчика затягиваются на месяцы. В условиях нашего бизнеса это очень долго. 

Подобная тенденция привела нас к мысли, что нужно все-таки открывать программу стажировки. Зачем? Мы решили, что уровень Junior+/Middle- вполне досягаем за полгода плотной работы над реальными проектами под менторством наших тимлидов.

Матрица компетенций, которую мы используем
Матрица компетенций, которую мы используем

В прошлом году мы взяли несколько стажеров «на пробу». Взяли активных ребят, с хорошим базовым пониманием Python, но без особого опыта в реальной разработке. Все они были разных возрастов — от 20 до 30 лет. Уровень навыков у них был «прошел курсы и работаю с Pet-проектами». Затем дали им несколько небольших внутренних проектов, чтобы посмотреть на скорость их обучения и навыки. После этого подключили их к реальным проектам и дали возможность расти. 

Результаты нас очень удивили. Из пятерых стажеров четверых мы пригласили в компанию. Все они показывали уровень качества не ниже, чем наши подрядчики. Поэтому в этом году мы решили подойти к стажировкам серьезнее: систематизировать программу обучения и дать ребятам более прозрачный Roadmap по стажировке.

Зачем же нужны стажеры?

В нашей компании качество на первом месте. Мы строим свою культуру разработки, ищем лучшие решения из нашего опыта, дорабатываем и переиспользуем их. У нас есть определенные требования к качеству ТЗ, реализованного кода, автотестам и безопасности. Эти требования и принципы приняты далеко не везде. 

Когда срочно требуются ресурсы, найти их на рынке зачастую невозможно. Тогда мы запрашиваем их у наших партнеров. Но важным фактором работы с подрядчиками является текучка мозгов. Работая над сложными проектами, мы, по сути, качаем внешние ресурсы, что не приводит к накоплению экспертизы в компании.

И мы сделали ставку на долгосрочный актив в виде стажировок.

Наша задача — найти хорошего начинающего разработчика, дать ему задачи и ментора и привить нашу культуру. 

Культура разработки — одна из основных наших целей как компании. Мы хотим, чтобы за код, который мы передаем нашему клиенту, было не стыдно. А такая культура невозможна без «благотворительности». Ей нужно делиться — только так мы увидим, где мы ошибаемся, где есть недочеты. Стажеры помогают обкатывать наши процессы, помогают выявить недостаточность информации, а зачастую преподносят что-то новое. 

Как оказалось, если найти адекватного сотрудника, пусть и без богатого опыта в разработке, то его прокачка занимает не так уж много времени. Мы сделали ставку на три месяца по стажировке, чтобы определить, кто на что способен. После этого мы в течение еще трех месяцев оттачиваем навыки сотрудника и ожидаем, что он дорастет до уровня Middle. При грамотном подходе к процессам это вполне достижимо. 

Еще одним хороший повод завести стажера (да простят они меня) — это прокачивание навыков коммуникации у ваших Senior и тимлидов. Процесс обучения требует определенных навыков: терпения, структуризации знаний, декомпозиции сложного на простое, умения объяснять. Эти навыки полезны во многих аспектах работы тимлида. И процесс обучения стажеров поможет прокачать или развить эти навыки. 

Работаем мы и над тем, чтобы стажер, даже если он не пройдет стажировку или решит уйти в другую компанию, обладал теми качествами, которые мы видим в хорошем разработчике: внимательность, дотошность к мелочам, умение гуглить и решать сложные задачи, писать грамотный код, покрытый тестами, использовать линтеры, учитывать пограничные значения и неявное поведение.

Где искать стажеров

1. Мы достаточно плотно работаем с обучающими платформами: SkillBox, SkillFactory, Coursera и др. Были даже кандидаты, которые проходили наши курсы на Нетологии и GeekBrains — они узнавали меня на собеседовании. На многих платформах дают неплохой теоретический материал, который стараются подтвердить практикой. И по опыту, люди, которые выходят с курсов, обладают достаточным набором навыков, чтобы быстро дойти до позиции Junior-разработчика.

2. Иногда берем людей и без курсов. Один из наших стажеров пришел в компанию в возрасте 30 лет из юридической области. Он плотно читал книги, пробовал онлайн-платформы с задачами по Python, делал свои небольшие приложения. В конце прошлого года он успешно прошел стажировку и до сих пор активно растет в нашей компании.

3. Я общался с некоторыми компаниями, которые ищут стажеров в университетах. У них этот подход систематизирован, и людей они ищут сразу после ЕГЭ. Стажеры проходят стажировку напрямую в подразделениях компании и с очень большой вероятностью остаются в компании на годы после завершения обучения.

При этом отбор студентов жесткий. Компании стараются вкладываться в студентов с высокими оценками по профильным предметам, отбирают студентов определенных университетов и тратят на это много ресурсов. Это достаточно большой и сложный путь. К тому же, на мой взгляд, весьма рискованный. Мы, возможно, пойдем по нему, но не сегодня 🙂

Главное в поисках стажера — отсечь тех, кому неинтересно программирование. Мы ищем именно горящие глаза, ребят, которые готовы посвятить всё свободное время развитию. 

При этом мы не занимаемся обучением стажеров. Мы лишь даем им возможность расти и обучаться самостоятельно. Как говорили мои учителя, невозможно научить, можно научиться. Мы стараемся дать стажерам возможности для роста: задачи, теоретический материал, — подсказать правильный путь, рассказать про подводные камни, помочь с ошибкой, чтобы стажер не тратил неделю на ее поиски. 

Как работаем со стажерами

Как и говорил, мы не обучаем стажеров, а лишь направляем их. Это не значит, что мы с порога бросаем их в котел программирования. Мы начинаем с общих вводных в работу программиста. Учим гуглить, рассказываем про основные инструменты разработчика (многие, например, не умеют работать с дебаг-режимом PyCharm), объясняем устройство основных систем и ресурсов.

Для стажеров мы разработали Roadmap, в котором перечислены все основные технологии, которые он опробует и освоит. Он достаточно большой и охватывает все прикладные технологии нашей компании — так мы укрепляем основы Python и Web. Входят туда и софт-скиллы: коммуникация, тайм-менеджмент, работа с TaskTrackers и т. п. При завершении стажировки сотрудник должен быть готов к любой ситуации.

Черновая версия Roadmap для стажеров
Черновая версия Roadmap для стажеров

После мы даем небольшие задачи на изучение фреймворков Django и FastAPI. Ребята сразу делают независимые сервисы, где потребуется поработать с основными особенностями каждого фреймворка, поработать с БД, пооптимизировать запросы, поработать с nginx и Docker, настроить CI/CD, написать автотесты и т. д. Стажер должен, по сути, подготовить готовый микросервис, который мы потом интегрируем в инфраструктуру нашей компании. 

После этого мы стараемся подключить стажера к нашим реальным проектам. Каждому прикрепляем ментора. Он направляет и помогает стажеру, дает ему задачи и контролирует качество выполнения. При этом стажер сразу же интегрируется в команду, с которой будет работать после завершения стажировки.

Задачи ментора — отвечать на вопросы и контролировать результаты. Поэтому каждый день мы проводим Traction Meetings со стажерами, чтобы у них была возможность поговорить и решить вопросы в реальном времени. В этом процессе главное — открытый подход. Чем понятнее и открытее вы будете к стажеру, тем проще ему будет интегрироваться в команду и работать над задачами.

Я не верю в полное систематизированное обучение по шаблонам. Индивидуальный подход к каждому стажеру показал свою эффективность, и мы остаемся на этом пути. Мы подходим к стажерам точно так же, как подходим к нашим разработчикам. Каждому нужен ИПР, каждому нужно оказать всестороннюю поддержку, чтобы разработчик мог эффективно работать и развиваться. 

Из системных подходов мы используем базу знаний нашей компании, которую постоянно пополняем. А еще пользуемся матрицей грейдов, где каждый навык декомпозирован. Матрица расшифровывает, какие технологии или ресурсы необходимо знать разработчику, чтобы претендовать на тот или иной грейд.

Каждый навык из матрицы компетенций подробно расписан для стажера
Каждый навык из матрицы компетенций подробно расписан для стажера

Ключевым моментом остаются дейлики, где стажер и тимлид должны прорабатывать сложные моменты. Это отличный бустер для стажера. Ведется и общий чат, где стажер в любое время может задать вопрос. В чате все менторы и все стажеры, чтобы была возможность коммуницировать не только в черный ящик. 

Какие результаты и текучка

За прошлый год я поработал с пятью стажерами. Среднее время стажировки составило три месяца. Из пятерых стажеров мы взяли четверых. Из них спустя год у нас осталось двое. Один из ушедших решил переключиться в ML и Data-аналитику. Ушел в Яндекс. Второй тоже решил уйти в Яндекс — изучать технологии, с которыми мы не работаем. 

Оставшиеся двое успешно работают над нашими проектами, развиваются и в скором времени, я надеюсь, дорастут до грейда Senior. 

Текучка, конечно, не низкая. В этом году отбор показал, что до конца стажировки доживают далеко не все. Многие отсеиваются в первые месяцы. Некоторые не могут уделять достаточное время, у других случаются форс-мажоры. В то же время из принятых стажеров уже появилось несколько перспективных ребят, которые в скором времени войдут в ряды нашей команды.

Количество стажеров

Количество заявок

Взяли на стажировку

Прошли стажировку

Остались в штате

87

23

15

6

Тенденции в компании 

Я заметил в нашей компании несколько трендов. Например, многие клиенты категорически отказываются иметь дело с начинающими разработчиками или разработчиками низких грейдов. Им интересны только специалисты грейдов Middle и выше.

При этом хороших «мидлов и выше» на рынке не так уж много. Начинающие специалисты оказываются за бортом. Даже умные и сильные ребята, но без реального опыта разработки, не могут попасть в среду для быстрого и эффективного роста. Это большой рынок кадров, к которому стоит присмотреться. Зачастую вырастить себе специалиста оказывается дешевле и эффективнее, чем тратить месяцы на поиск, а потом месяцы на дообучение человека «с опытом». 

Мы решили найти самородков на этом рынке и взрастить себе кадры. И этот путь оказался верным. С ростом компании, мы не можем оперативно закрывать кадровые вопросы. Стажировки дали еще один дополнительный, пусть и долгий, источник ресурсов.

В будущем мы планируем расширять программу. Брать более слабых разработчиков, продлевая им стажировку, пока они не достигнут необходимого уровня. Кратно увеличить количество стажеров в компании. Доработать все процессы, выстроить инфраструктуру для их роста. Это большая и сложная работа, но мы верим в эту идею и реализуем ее.

Если хотите стать стажером в AGIMA, присылайте резюме на hr@agima.ru. К резюме приложите небольшой рассказ о себе.

Расскажите в комментариях, были ли вы сами стажером? Есть ли в вашей компании стажировки? С какими проблемами вы сталкивались, когда сами были стажером? Или с какими проблемами столкнулась ваша компания, работая со стажерами?


ссылка на оригинал статьи https://habr.com/ru/company/agima/blog/685666/