From 2489b8b5cfd8fc17c7cbf4181e516e810e58bcd6 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 06:37:16 +0400 Subject: [PATCH 01/17] =?UTF-8?q?=D0=9E=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=81=D0=B2=D1=8F=D0=B7=D0=B8:=20=D0=B0=D0=B4=D1=80=D0=B5?= =?UTF-8?q?=D1=81=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D1=87=D0=B8=D0=BD=D0=B0=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20=C2=AB=D0=9D=D0=B5=D1=82=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Модуль ОшибкиСети переводит исключение HTTP-соединения в причину (имя не разрешается, соединение отклонено, таймаут, TLS) и снимает служебную обёртку платформы. СерверПакетов запоминает ПоследнююОшибкуСети; установка различает 404, 401/403 и прочие коды ответа, а для единственного сервера (-m) называет адрес и причину. opm.os показывает суть исключения, подробности — в отладке. --- README.md | 16 ++ packagedef | 1 + src/cmd/opm.os | 5 +- ...20\272\320\265\321\202\320\276\320\262.os" | 46 ++++-- ...20\272\320\265\321\202\320\276\320\262.os" | 35 +++-- ...20\270\320\241\320\265\321\202\320\270.os" | 105 +++++++++++++ tests/network-errors-test.os | 139 ++++++++++++++++++ 7 files changed, 326 insertions(+), 21 deletions(-) create mode 100644 "src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" create mode 100644 tests/network-errors-test.os diff --git a/README.md b/README.md index 4bc1bcc..5dc9483 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,22 @@ OneScript Package Manager Справка из википедии: [Система управления пакетами](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D0%BF%D0%B0%D0%BA%D0%B5%D1%82%D0%B0%D0%BC%D0%B8) +# Ошибки связи + +Если до сервера пакетов не удалось достучаться, opm называет его адрес и причину: +неразрешимое имя, отказ в соединении, таймаут, сбой TLS. + +```console +$ opm install -m МойХаб my-package +Ошибка установки пакета my-package: Не удалось подключиться к http://hub.example.com:3380: имя хоста не разрешается +``` + +Если хаб ответил, но файла не отдал, в сообщении стоит код ответа: 404 — файл не найден, +401 и 403 — отказ в доступе. При переборе нескольких серверов из `opm.cfg` недоступный сервер +остаётся предупреждением, и поиск идёт дальше. Служебная обёртка платформы (имя модуля, +номер строки, тип .NET-исключения) в сообщение не попадает — она уходит в отладочный журнал +`oscript.app.opm`. + # Сервера В настоящее время пакеты хранятся на двух серверах: - [hub.oscript.io](http://hub.oscript.io/download) - основной хаб пакетов diff --git a/packagedef b/packagedef index f39977b..2b7bb3a 100644 --- a/packagedef +++ b/packagedef @@ -49,6 +49,7 @@ .ОпределяетКласс("УстановкаПакета", "src/core/Классы/УстановкаПакета.os") .ОпределяетМодуль("КонстантыOpm", "src/core/Модули/КонстантыOpm.os") .ОпределяетМодуль("НастройкиOpm", "src/core/Модули/НастройкиOpm.os") + .ОпределяетМодуль("ОшибкиСети", "src/core/Модули/ОшибкиСети.os") .ОпределяетМодуль("РаботаСВерсиями", "src/core/Модули/РаботаСВерсиями.os") .ОпределяетМодуль("РаботаСОписаниемПакета", "src/core/Модули/РаботаСОписаниемПакета.os") .ОпределяетМодуль("РаботаСПакетами", "src/core/Модули/РаботаСПакетами.os") diff --git a/src/cmd/opm.os b/src/cmd/opm.os index 37ed044..5f02223 100644 --- a/src/cmd/opm.os +++ b/src/cmd/opm.os @@ -11,6 +11,7 @@ #Использовать "../core" #Использовать "." #Использовать cli +#Использовать logos Перем ЭтоWindows; @@ -63,7 +64,9 @@ ЗавершитьРаботу(0); Исключение - Сообщить(ОписаниеОшибки()); + ПодробноеОписание = ОписаниеОшибки(); + Логирование.ПолучитьЛог(КонстантыOpm.ИмяЛога).Отладка(ПодробноеОписание); + Сообщить(ОшибкиСети.Суть(ПодробноеОписание)); ВременныеФайлы.Удалить(); diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 7355e45..a709db3 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -39,7 +39,8 @@ ПереченьСерверов = ИндексКешаПакетов[ИмяПакета]; - Ответ = ЗапроситьПакет(ПереченьСерверов, ИмяСервера, ИмяРесурса); + Полученное = ЗапроситьПакет(ПереченьСерверов, ИмяСервера, ИмяРесурса); + Ответ = Полученное.Ответ; Если Не Ответ = Неопределено Тогда Лог.Отладка("Файл получен"); @@ -47,7 +48,7 @@ Ответ.Закрыть(); Лог.Отладка("Соединение закрыто"); Иначе - ТекстИсключения = СтрШаблон("Ошибка установки пакета %1: Нет соединения", ИмяПакета); + ТекстИсключения = СтрШаблон("Ошибка установки пакета %1: %2", ИмяПакета, ПричинаНеудачи(ФайлПакета, Полученное)); ВызватьИсключение ТекстИсключения; КонецЕсли; @@ -55,16 +56,41 @@ КонецФункции +Функция ПричинаНеудачи(Знач ФайлПакета, Знач Полученное) + + КодСостояния = Полученное.КодСостояния; + Если КодСостояния = 404 Тогда + Возврат СтрШаблон("файл %1 в хабе не найден", ФайлПакета); + ИначеЕсли КодСостояния = 401 ИЛИ КодСостояния = 403 Тогда + Возврат СтрШаблон("хаб отказал в доступе к %1 (код %2)", ФайлПакета, Формат(КодСостояния, "ЧГ=0")); + ИначеЕсли КодСостояния <> 0 Тогда + Возврат СтрШаблон("хаб ответил кодом %1 на запрос %2", Формат(КодСостояния, "ЧГ=0"), ФайлПакета); + ИначеЕсли ЗначениеЗаполнено(Полученное.ОшибкаСети) Тогда + Возврат Полученное.ОшибкаСети; + КонецЕсли; + + Возврат "Нет соединения"; + +КонецФункции + +// Возвращаемое значение: +// Структура - Ответ (HTTPОтвет, Неопределено), КодСостояния (Число, 0 — ответа не было), +// ОшибкаСети (Строка - сообщение о неудачном подключении к единственному названному серверу). Функция ЗапроситьПакет(Знач ПереченьСерверов, Знач ИмяСервера, Знач ИмяРесурса) ПакетУспешноПолучен = Ложь; ОтветСервера = Неопределено; + КодСостояния = 0; + ОшибкаСети = ""; // Если указан нужный сервер, то только его и будем использовать Если ЗначениеЗаполнено(ИмяСервера) Тогда Сервер = ИндексСерверовПакетов[ИмяСервера]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса); + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, КодСостояния); + Если ОтветСервера = Неопределено И ТипЗнч(Сервер) = Тип("СерверПакетов") Тогда + ОшибкаСети = Сервер.ПоследняяОшибкаСети; + КонецЕсли; Иначе @@ -72,7 +98,7 @@ Для Каждого ДоступныйСервер Из ПереченьСерверов Цикл Сервер = ИндексСерверовПакетов[ДоступныйСервер.Ключ]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса); + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, КодСостояния); Если ОтветСервера <> Неопределено Тогда Прервать; @@ -86,15 +112,14 @@ Если ОтветСервера <> Неопределено Тогда Лог.Отладка("Ресурс %1 успешно получен с %2", ИмяРесурса, Сервер.ПолучитьИмя()); - Возврат ОтветСервера; КонецЕсли; - Возврат Неопределено; + Возврат Новый Структура("Ответ, КодСостояния, ОшибкаСети", ОтветСервера, КодСостояния, ОшибкаСети); КонецФункции -Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса) +Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса, КодСостояния) ОтветСервера = Неопределено; Если Сервер.СерверДоступен() Тогда @@ -102,6 +127,8 @@ ОтветСервера = Сервер.ПолучитьРесурс(ИмяРесурса); Если ОтветСервера <> Неопределено Тогда + КодСостояния = ОтветСервера.КодСостояния; + Если ОтветСервера.КодСостояния = 200 Тогда Возврат ОтветСервера; @@ -110,9 +137,10 @@ ОтветСервера.Закрыть(); - Лог.Информация("Ошибка подключения к хабу %1 <%2>", + Лог.Отладка("Хаб %1 ответил кодом %2 на запрос %3", Сервер.ПолучитьИмя(), - ОтветСервера.КодСостояния); + КодСостояния, + ИмяРесурса); ОтветСервера = Неопределено; diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 368fe0a..7126906 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -10,6 +10,7 @@ Перем Авторизация Экспорт; Перем ДополнительныеЗаголовки Экспорт; Перем Таймаут Экспорт; +Перем ПоследняяОшибкаСети Экспорт; Перем ПакетыХаба; @@ -26,6 +27,7 @@ Авторизация = ВходящаяАвторизация; ДополнительныеЗаголовки = Заголовки; Таймаут = ВходящийТаймаут; + ПоследняяОшибкаСети = ""; КонецПроцедуры @@ -63,6 +65,7 @@ Ресурс = ПутьНаСервере + ИмяРесурса; Запрос = Новый HTTPЗапрос(Ресурс); ДобавитьЗаголовки(Запрос); + ПоследняяОшибкаСети = ""; Попытка @@ -70,13 +73,27 @@ Исключение - Лог.Ошибка(ОписаниеОшибки()); + ПодробноеОписание = ОписаниеОшибки(); + ПоследняяОшибкаСети = ОшибкиСети.НеУдалосьПодключиться(АдресДляСообщений(), ПодробноеОписание); + Лог.Отладка("Обращение к %1%2 не удалось: %3", Сервер, Ресурс, ПодробноеОписание); Возврат Неопределено; КонецПопытки; КонецФункции +// Адрес сервера в сообщениях пользователю: порт указывается, только если он не стандартный. +Функция АдресДляСообщений() + + СтандартныйПорт = ?(СтрНачинаетсяС(НРег(Сервер), "https://"), 443, 80); + Если Порт = 0 ИЛИ Порт = СтандартныйПорт Тогда + Возврат Сервер; + КонецЕсли; + + Возврат СтрШаблон("%1:%2", Сервер, Формат(Порт, "ЧГ=0")); + +КонецФункции + Функция ПрочитатьФайлСпискаПакетов(Текст) ТекстовыйДокумент = Новый ТекстовыйДокумент; @@ -101,15 +118,14 @@ Ответ = ПолучитьРесурс(Ресурс); Если Ответ = Неопределено Тогда - ТекстИсключения = "Ошибка подключения к зеркалу"; - ВызватьИсключение ТекстИсключения; + ВызватьИсключение ПоследняяОшибкаСети; КонецЕсли; Если Ответ.КодСостояния <> 200 Тогда ТекстОтвета = Ответ.ПолучитьТелоКакСтроку(); - ТекстИсключения = СтрШаблон("Ошибка подключения к зеркалу код ответа: <%1> - |Текст ответа: <%2>", Ответ.КодСостояния, ТекстОтвета); - ВызватьИсключение ТекстИсключения; + Ответ.Закрыть(); + ВызватьИсключение СтрШаблон("%1 ответил кодом %2 на запрос списка пакетов: %3", + АдресДляСообщений(), Формат(Ответ.КодСостояния, "ЧГ=0"), СокрЛП(ТекстОтвета)); КонецЕсли; ТекстОтвета = Ответ.ПолучитьТелоКакСтроку(); Ответ.Закрыть(); @@ -127,11 +143,8 @@ Попытка ТекстОтвета = ПолучитьСписокПакетов("list.txt"); Исключение - Лог.Предупреждение( - СтрШаблон("Ошибка получения списка пакетов с хаба %1 по причине %2", - Имя, ОписаниеОшибки() - ) - ); + Лог.Предупреждение("Список пакетов сервера %1 не получен: %2", + Имя, ОшибкиСети.Суть(ОписаниеОшибки())); КонецПопытки; ПрочитатьФайлСпискаПакетов(ТекстОтвета); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" new file mode 100644 index 0000000..8c39434 --- /dev/null +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" @@ -0,0 +1,105 @@ +// Перевод исключений HTTP-соединения в причину для пользователя. + +// Причина неудачного обращения к серверу одной фразой. +// +// Параметры: +// ТекстОшибки - Строка - ОписаниеОшибки() платформы. +// +// Возвращаемое значение: +// Строка - причина: имя не разрешается, соединение отклонено, таймаут, TLS и т.п. +// +Функция Причина(Знач ТекстОшибки) Экспорт + + Суть = Суть(ТекстОшибки); + Ниж = НРег(Суть); + + Если Содержит(Ниж, "nodename nor servname") + ИЛИ Содержит(Ниж, "no such host") + ИЛИ Содержит(Ниж, "name or service not known") + ИЛИ Содержит(Ниж, "getaddrinfo") + ИЛИ Содержит(Ниж, "host not found") + ИЛИ Содержит(Ниж, "nameresolution") Тогда + Возврат "имя хоста не разрешается"; + КонецЕсли; + + Если Содержит(Ниж, "connection refused") ИЛИ Содержит(Ниж, "actively refused") Тогда + Возврат "соединение отклонено — по этому адресу никто не слушает"; + КонецЕсли; + + Если Содержит(Ниж, "timed out") ИЛИ Содержит(Ниж, "timeout") Тогда + Возврат "истекло время ожидания ответа"; + КонецЕсли; + + Если Содержит(Ниж, "ssl") ИЛИ Содержит(Ниж, "tls") + ИЛИ Содержит(Ниж, "certificate") ИЛИ Содержит(Ниж, "secure channel") Тогда + Возврат "не удалось установить защищённое соединение (TLS)"; + КонецЕсли; + + Если Содержит(Ниж, "unreachable") Тогда + Возврат "сеть недоступна"; + КонецЕсли; + + Если Содержит(Ниж, "forcibly closed") ИЛИ Содержит(Ниж, "connection reset") Тогда + Возврат "соединение разорвано другой стороной"; + КонецЕсли; + + Возврат ?(ПустаяСтрока(Суть), "соединение не установлено", Суть); + +КонецФункции + +// Сообщение о неудачном подключении к серверу. +// +// Параметры: +// Адрес - Строка - адрес сервера. +// ТекстОшибки - Строка - ОписаниеОшибки() платформы. +// +// Возвращаемое значение: +// Строка - "Не удалось подключиться к <адрес>: <причина>". +// +Функция НеУдалосьПодключиться(Знач Адрес, Знач ТекстОшибки) Экспорт + + Возврат СтрШаблон("Не удалось подключиться к %1: %2", Адрес, Причина(ТекстОшибки)); + +КонецФункции + +// Текст исключения без служебной обёртки платформы: имени модуля, номера строки и типа .NET-исключения. +// +// Параметры: +// ТекстОшибки - Строка - ОписаниеОшибки() платформы. +// +// Возвращаемое значение: +// Строка - суть сообщения. +// +Функция Суть(Знач ТекстОшибки) Экспорт + + Исходный = СокрЛП(Строка(ТекстОшибки)); + + // тело ответа хаба тоже начинается с фигурной скобки, поэтому нужны обе приметы + Если НЕ СтрНачинаетсяС(Исходный, "{Модуль ") ИЛИ СтрНайти(Исходный, " / Error in line") = 0 Тогда + Возврат Исходный; + КонецЕсли; + + Значение = СокрЛП(СтрЗаменить(СтрЗаменить(Исходный, Символы.ПС, " "), Символы.ВК, "")); + Если СтрЗаканчиваетсяНа(Значение, "}") Тогда + Значение = СокрЛП(Лев(Значение, СтрДлина(Значение) - 1)); + КонецЕсли; + + // "{Модуль …/Файл.os / Error in line: 69 / текст}" + Позиция = СтрНайти(Значение, " / ", НаправлениеПоиска.СКонца); + Если Позиция > 0 Тогда + Значение = СокрЛП(Сред(Значение, Позиция + 3)); + КонецЕсли; + + // "Внешнее исключение (System.Net.WebException): текст" + Позиция = СтрНайти(Значение, "):"); + Если СтрНачинаетсяС(Значение, "Внешнее исключение") И Позиция > 0 Тогда + Значение = СокрЛП(Сред(Значение, Позиция + 2)); + КонецЕсли; + + Возврат Значение; + +КонецФункции + +Функция Содержит(Знач Строка_, Знач Подстрока) + Возврат СтрНайти(Строка_, Подстрока) > 0; +КонецФункции diff --git a/tests/network-errors-test.os b/tests/network-errors-test.os new file mode 100644 index 0000000..5aabbd7 --- /dev/null +++ b/tests/network-errors-test.os @@ -0,0 +1,139 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_УзнатьНеразрешимоеИмяХоста"); + СписокТестов.Добавить("ТестДолжен_УзнатьОтказВСоединении"); + СписокТестов.Добавить("ТестДолжен_УзнатьТаймаут"); + СписокТестов.Добавить("ТестДолжен_УзнатьОшибкуTLS"); + СписокТестов.Добавить("ТестДолжен_ВернутьСутьНеузнаннойОшибки"); + СписокТестов.Добавить("ТестДолжен_СнятьСлужебнуюОбёрткуПлатформы"); + СписокТестов.Добавить("ТестДолжен_НеТрогатьТекстБезОбёртки"); + СписокТестов.Добавить("ТестДолжен_НеЛоматьТелоОтветаХаба"); + СписокТестов.Добавить("ТестДолжен_СобратьСообщениеОНеудачномПодключении"); + СписокТестов.Добавить("ТестДолжен_ЗапомнитьОшибкуСетиСервераПакетов"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт +КонецПроцедуры + +Процедура ТестДолжен_УзнатьНеразрешимоеИмяХоста() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина(ОбёрнутьКакПлатформа( + "Внешнее исключение (System.Net.WebException): nodename nor servname provided," + + " or not known (lacalhost:3380)")), "macOS").Равно("имя хоста не разрешается"); + + Ожидаем.Что(ОшибкиСети.Причина("The remote name could not be resolved: No such host is known"), "Windows") + .Равно("имя хоста не разрешается"); + + Ожидаем.Что(ОшибкиСети.Причина("Name or service not known"), "Linux") + .Равно("имя хоста не разрешается"); + +КонецПроцедуры + +Процедура ТестДолжен_УзнатьОтказВСоединении() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина(ОбёрнутьКакПлатформа( + "Внешнее исключение (System.Net.WebException): Connection refused [::ffff:127.0.0.1]:9 (127.0.0.1:9)")), + "POSIX").Равно("соединение отклонено — по этому адресу никто не слушает"); + + Ожидаем.Что(ОшибкиСети.Причина("No connection could be made because the target machine actively refused it"), + "Windows").Равно("соединение отклонено — по этому адресу никто не слушает"); + +КонецПроцедуры + +Процедура ТестДолжен_УзнатьТаймаут() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина("The operation has timed out"), "таймаут") + .Равно("истекло время ожидания ответа"); + +КонецПроцедуры + +Процедура ТестДолжен_УзнатьОшибкуTLS() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина("The SSL connection could not be established"), "TLS") + .Равно("не удалось установить защищённое соединение (TLS)"); + + Ожидаем.Что(ОшибкиСети.Причина("The remote certificate is invalid according to the validation procedure"), + "сертификат").Равно("не удалось установить защищённое соединение (TLS)"); + +КонецПроцедуры + +Процедура ТестДолжен_ВернутьСутьНеузнаннойОшибки() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина(ОбёрнутьКакПлатформа("Внешнее исключение (System.Exception): что-то своё")), + "неузнанная причина").Равно("что-то своё"); + + Ожидаем.Что(ОшибкиСети.Причина(""), "пустой текст").Равно("соединение не установлено"); + +КонецПроцедуры + +Процедура ТестДолжен_СнятьСлужебнуюОбёрткуПлатформы() Экспорт + + Обёрнутое = "{Модуль /путь/к/СерверПакетов.os / Error in line: 69 /" + + " Внешнее исключение (System.Net.WebException): Connection refused}"; + + Суть = ОшибкиСети.Суть(Обёрнутое); + + Ожидаем.Что(Суть, "суть без модуля и строки").Равно("Connection refused"); + Ожидаем.Что(СтрНайти(Суть, "Модуль"), "имени модуля быть не должно").Равно(0); + Ожидаем.Что(СтрНайти(Суть, "Error in line"), "номера строки быть не должно").Равно(0); + Ожидаем.Что(СтрНайти(Суть, "System.Net"), "типа исключения быть не должно").Равно(0); + +КонецПроцедуры + +Процедура ТестДолжен_НеТрогатьТекстБезОбёртки() Экспорт + + Ожидаем.Что(ОшибкиСети.Суть("Пакет не найден"), "обычное сообщение").Равно("Пакет не найден"); + +КонецПроцедуры + +// Тело ответа хаба тоже начинается с фигурной скобки — его переносы строк ломать нельзя. +Процедура ТестДолжен_НеЛоматьТелоОтветаХаба() Экспорт + + Тело = "{" + Символы.ПС + """КодСостояние"": 409," + Символы.ПС + """Сообщение"": ""нельзя""" + Символы.ПС + "}"; + + Ожидаем.Что(ОшибкиСети.Суть(Тело), "JSON остаётся как есть").Равно(Тело); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьСообщениеОНеудачномПодключении() Экспорт + + Сообщение = ОшибкиСети.НеУдалосьПодключиться("http://lacalhost:3380", + ОбёрнутьКакПлатформа("Внешнее исключение (System.Net.WebException): nodename nor servname provided")); + + Ожидаем.Что(Сообщение, "готовое сообщение") + .Равно("Не удалось подключиться к http://lacalhost:3380: имя хоста не разрешается"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапомнитьОшибкуСетиСервераПакетов() Экспорт + + // на порту 9 (discard) в тестовом окружении никто не слушает + Сервер = Новый СерверПакетов("закрытый", "http://127.0.0.1", "/", "/", 9); + + Ожидаем.Что(Сервер.ПолучитьРесурс("list.txt"), "ответа нет").Равно(Неопределено); + Ожидаем.Что(Сервер.ПоследняяОшибкаСети, "сообщение с адресом и причиной") + .Равно("Не удалось подключиться к http://127.0.0.1:9: соединение отклонено — по этому адресу никто не слушает"); + + // недоступный сервер при получении списка пакетов только предупреждает + Сервер.ПолучитьПакеты(); + +КонецПроцедуры + +Функция ОбёрнутьКакПлатформа(Знач Текст) + Возврат СтрШаблон("{Модуль /путь/к/Модулю.os / Error in line: 42 / %1}", Текст); +КонецФункции From cf0028cf7c642b5e4e88aa14ec5a04f922c8c8e4 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 06:38:46 +0400 Subject: [PATCH 02/17] =?UTF-8?q?=D0=9E=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=81=D0=B5=D1=82=D0=B8:=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=20=D1=82=D0=B5=D0=BD=D0=B8=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D0=B8=20=D0=A1=D1=83=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\270\320\241\320\265\321\202\320\270.os" | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" index 8c39434..a784ef6 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" @@ -10,40 +10,40 @@ // Функция Причина(Знач ТекстОшибки) Экспорт - Суть = Суть(ТекстОшибки); - Ниж = НРег(Суть); - - Если Содержит(Ниж, "nodename nor servname") - ИЛИ Содержит(Ниж, "no such host") - ИЛИ Содержит(Ниж, "name or service not known") - ИЛИ Содержит(Ниж, "getaddrinfo") - ИЛИ Содержит(Ниж, "host not found") - ИЛИ Содержит(Ниж, "nameresolution") Тогда + Сообщение = Суть(ТекстОшибки); + Текст = НРег(Сообщение); + + Если Содержит(Текст, "nodename nor servname") + ИЛИ Содержит(Текст, "no such host") + ИЛИ Содержит(Текст, "name or service not known") + ИЛИ Содержит(Текст, "getaddrinfo") + ИЛИ Содержит(Текст, "host not found") + ИЛИ Содержит(Текст, "nameresolution") Тогда Возврат "имя хоста не разрешается"; КонецЕсли; - Если Содержит(Ниж, "connection refused") ИЛИ Содержит(Ниж, "actively refused") Тогда + Если Содержит(Текст, "connection refused") ИЛИ Содержит(Текст, "actively refused") Тогда Возврат "соединение отклонено — по этому адресу никто не слушает"; КонецЕсли; - Если Содержит(Ниж, "timed out") ИЛИ Содержит(Ниж, "timeout") Тогда + Если Содержит(Текст, "timed out") ИЛИ Содержит(Текст, "timeout") Тогда Возврат "истекло время ожидания ответа"; КонецЕсли; - Если Содержит(Ниж, "ssl") ИЛИ Содержит(Ниж, "tls") - ИЛИ Содержит(Ниж, "certificate") ИЛИ Содержит(Ниж, "secure channel") Тогда + Если Содержит(Текст, "ssl") ИЛИ Содержит(Текст, "tls") + ИЛИ Содержит(Текст, "certificate") ИЛИ Содержит(Текст, "secure channel") Тогда Возврат "не удалось установить защищённое соединение (TLS)"; КонецЕсли; - Если Содержит(Ниж, "unreachable") Тогда + Если Содержит(Текст, "unreachable") Тогда Возврат "сеть недоступна"; КонецЕсли; - Если Содержит(Ниж, "forcibly closed") ИЛИ Содержит(Ниж, "connection reset") Тогда + Если Содержит(Текст, "forcibly closed") ИЛИ Содержит(Текст, "connection reset") Тогда Возврат "соединение разорвано другой стороной"; КонецЕсли; - Возврат ?(ПустаяСтрока(Суть), "соединение не установлено", Суть); + Возврат ?(ПустаяСтрока(Сообщение), "соединение не установлено", Сообщение); КонецФункции @@ -100,6 +100,6 @@ КонецФункции -Функция Содержит(Знач Строка_, Знач Подстрока) - Возврат СтрНайти(Строка_, Подстрока) > 0; +Функция Содержит(Знач Текст, Знач Подстрока) + Возврат СтрНайти(Текст, Подстрока) > 0; КонецФункции From 5f937f3dbccba4a8e689a6b9ad5a2117dc46bc16 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 09:50:46 +0400 Subject: [PATCH 03/17] =?UTF-8?q?=D0=9E=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=81=D0=B2=D1=8F=D0=B7=D0=B8=20=D0=BF=D0=BE=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4=D1=83=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0:=20404=20?= =?UTF-8?q?=E2=80=94=20=D1=84=D0=B0=D0=B9=D0=BB=20=D0=BD=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B9=D0=B4=D0=B5=D0=BD,=20401=20=D0=B8=20403=20?= =?UTF-8?q?=E2=80=94=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=20=D0=B2=20=D0=B4?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=B5,=20=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D1=87=D0=B5=20=D0=BA=D0=BE=D0=B4=20=D0=B8=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB;=20=D0=B1=D0=B5=D0=B7=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=B0=20=E2=80=94=20=C2=AB=D0=9D=D0=B5=20=D1=83=D0=B4=D0=B0?= =?UTF-8?q?=D0=BB=D0=BE=D1=81=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=D1=81=D1=8F=20=D0=BA=20<=D0=B0=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D1=81>=C2=BB,=20=D0=B8=D1=81=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B2=20=D0=BE=D1=82=D0=BB=D0=B0=D0=B4=D0=BE=D1=87=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB;=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D1=83=D0=BB=D1=8C=20=D0=9E=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= =?UTF-8?q?=D0=A1=D0=B5=D1=82=D0=B8=20=D0=B8=20=D1=80=D0=B0=D0=B7=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B0=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B9=20=D1=81?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +-- packagedef | 1 - src/cmd/opm.os | 5 +- ...20\272\320\265\321\202\320\276\320\262.os" | 12 +- ...20\270\320\241\320\265\321\202\320\270.os" | 105 -------------- ...20\260\320\245\320\260\320\261\320\260.os" | 12 ++ ...20\272\320\265\321\202\320\276\320\262.os" | 35 +++++ tests/network-errors-test.os | 134 ++++++------------ 8 files changed, 107 insertions(+), 213 deletions(-) delete mode 100644 "src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" create mode 100644 "tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" create mode 100644 "tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" diff --git a/README.md b/README.md index 5dc9483..0af47ef 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,17 @@ OneScript Package Manager # Ошибки связи -Если до сервера пакетов не удалось достучаться, opm называет его адрес и причину: -неразрешимое имя, отказ в соединении, таймаут, сбой TLS. +Если хаб ответил, но файла не отдал, причина называется по коду ответа: 404 — файл не найден, +401 и 403 — отказ в доступе, иначе — код ответа и имя файла. ```console -$ opm install -m МойХаб my-package -Ошибка установки пакета my-package: Не удалось подключиться к http://hub.example.com:3380: имя хоста не разрешается +$ opm install my-package +Ошибка установки пакета my-package: файл my-package.ospx в хабе не найден ``` -Если хаб ответил, но файла не отдал, в сообщении стоит код ответа: 404 — файл не найден, -401 и 403 — отказ в доступе. При переборе нескольких серверов из `opm.cfg` недоступный сервер -остаётся предупреждением, и поиск идёт дальше. Служебная обёртка платформы (имя модуля, -номер строки, тип .NET-исключения) в сообщение не попадает — она уходит в отладочный журнал -`oscript.app.opm`. +Если ответа нет, opm называет адрес сервера: `Не удалось подключиться к http://hub.example.com:3380`. +Исходное описание ошибки уходит в отладочный журнал `oscript.app.opm`. При переборе нескольких +серверов из `opm.cfg` недоступный сервер остаётся предупреждением, и поиск идёт дальше. # Сервера В настоящее время пакеты хранятся на двух серверах: diff --git a/packagedef b/packagedef index 2b7bb3a..f39977b 100644 --- a/packagedef +++ b/packagedef @@ -49,7 +49,6 @@ .ОпределяетКласс("УстановкаПакета", "src/core/Классы/УстановкаПакета.os") .ОпределяетМодуль("КонстантыOpm", "src/core/Модули/КонстантыOpm.os") .ОпределяетМодуль("НастройкиOpm", "src/core/Модули/НастройкиOpm.os") - .ОпределяетМодуль("ОшибкиСети", "src/core/Модули/ОшибкиСети.os") .ОпределяетМодуль("РаботаСВерсиями", "src/core/Модули/РаботаСВерсиями.os") .ОпределяетМодуль("РаботаСОписаниемПакета", "src/core/Модули/РаботаСОписаниемПакета.os") .ОпределяетМодуль("РаботаСПакетами", "src/core/Модули/РаботаСПакетами.os") diff --git a/src/cmd/opm.os b/src/cmd/opm.os index 5f02223..37ed044 100644 --- a/src/cmd/opm.os +++ b/src/cmd/opm.os @@ -11,7 +11,6 @@ #Использовать "../core" #Использовать "." #Использовать cli -#Использовать logos Перем ЭтоWindows; @@ -64,9 +63,7 @@ ЗавершитьРаботу(0); Исключение - ПодробноеОписание = ОписаниеОшибки(); - Логирование.ПолучитьЛог(КонстантыOpm.ИмяЛога).Отладка(ПодробноеОписание); - Сообщить(ОшибкиСети.Суть(ПодробноеОписание)); + Сообщить(ОписаниеОшибки()); ВременныеФайлы.Удалить(); diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 7126906..66c45e1 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -73,9 +73,8 @@ Исключение - ПодробноеОписание = ОписаниеОшибки(); - ПоследняяОшибкаСети = ОшибкиСети.НеУдалосьПодключиться(АдресДляСообщений(), ПодробноеОписание); - Лог.Отладка("Обращение к %1%2 не удалось: %3", Сервер, Ресурс, ПодробноеОписание); + ПоследняяОшибкаСети = СтрШаблон("Не удалось подключиться к %1", АдресДляСообщений()); + Лог.Отладка("Обращение к %1%2 не удалось: %3", Сервер, Ресурс, ОписаниеОшибки()); Возврат Неопределено; КонецПопытки; @@ -143,8 +142,11 @@ Попытка ТекстОтвета = ПолучитьСписокПакетов("list.txt"); Исключение - Лог.Предупреждение("Список пакетов сервера %1 не получен: %2", - Имя, ОшибкиСети.Суть(ОписаниеОшибки())); + Лог.Предупреждение( + СтрШаблон("Ошибка получения списка пакетов с хаба %1 по причине %2", + Имя, ОписаниеОшибки() + ) + ); КонецПопытки; ПрочитатьФайлСпискаПакетов(ТекстОтвета); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" deleted file mode 100644 index a784ef6..0000000 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" +++ /dev/null @@ -1,105 +0,0 @@ -// Перевод исключений HTTP-соединения в причину для пользователя. - -// Причина неудачного обращения к серверу одной фразой. -// -// Параметры: -// ТекстОшибки - Строка - ОписаниеОшибки() платформы. -// -// Возвращаемое значение: -// Строка - причина: имя не разрешается, соединение отклонено, таймаут, TLS и т.п. -// -Функция Причина(Знач ТекстОшибки) Экспорт - - Сообщение = Суть(ТекстОшибки); - Текст = НРег(Сообщение); - - Если Содержит(Текст, "nodename nor servname") - ИЛИ Содержит(Текст, "no such host") - ИЛИ Содержит(Текст, "name or service not known") - ИЛИ Содержит(Текст, "getaddrinfo") - ИЛИ Содержит(Текст, "host not found") - ИЛИ Содержит(Текст, "nameresolution") Тогда - Возврат "имя хоста не разрешается"; - КонецЕсли; - - Если Содержит(Текст, "connection refused") ИЛИ Содержит(Текст, "actively refused") Тогда - Возврат "соединение отклонено — по этому адресу никто не слушает"; - КонецЕсли; - - Если Содержит(Текст, "timed out") ИЛИ Содержит(Текст, "timeout") Тогда - Возврат "истекло время ожидания ответа"; - КонецЕсли; - - Если Содержит(Текст, "ssl") ИЛИ Содержит(Текст, "tls") - ИЛИ Содержит(Текст, "certificate") ИЛИ Содержит(Текст, "secure channel") Тогда - Возврат "не удалось установить защищённое соединение (TLS)"; - КонецЕсли; - - Если Содержит(Текст, "unreachable") Тогда - Возврат "сеть недоступна"; - КонецЕсли; - - Если Содержит(Текст, "forcibly closed") ИЛИ Содержит(Текст, "connection reset") Тогда - Возврат "соединение разорвано другой стороной"; - КонецЕсли; - - Возврат ?(ПустаяСтрока(Сообщение), "соединение не установлено", Сообщение); - -КонецФункции - -// Сообщение о неудачном подключении к серверу. -// -// Параметры: -// Адрес - Строка - адрес сервера. -// ТекстОшибки - Строка - ОписаниеОшибки() платформы. -// -// Возвращаемое значение: -// Строка - "Не удалось подключиться к <адрес>: <причина>". -// -Функция НеУдалосьПодключиться(Знач Адрес, Знач ТекстОшибки) Экспорт - - Возврат СтрШаблон("Не удалось подключиться к %1: %2", Адрес, Причина(ТекстОшибки)); - -КонецФункции - -// Текст исключения без служебной обёртки платформы: имени модуля, номера строки и типа .NET-исключения. -// -// Параметры: -// ТекстОшибки - Строка - ОписаниеОшибки() платформы. -// -// Возвращаемое значение: -// Строка - суть сообщения. -// -Функция Суть(Знач ТекстОшибки) Экспорт - - Исходный = СокрЛП(Строка(ТекстОшибки)); - - // тело ответа хаба тоже начинается с фигурной скобки, поэтому нужны обе приметы - Если НЕ СтрНачинаетсяС(Исходный, "{Модуль ") ИЛИ СтрНайти(Исходный, " / Error in line") = 0 Тогда - Возврат Исходный; - КонецЕсли; - - Значение = СокрЛП(СтрЗаменить(СтрЗаменить(Исходный, Символы.ПС, " "), Символы.ВК, "")); - Если СтрЗаканчиваетсяНа(Значение, "}") Тогда - Значение = СокрЛП(Лев(Значение, СтрДлина(Значение) - 1)); - КонецЕсли; - - // "{Модуль …/Файл.os / Error in line: 69 / текст}" - Позиция = СтрНайти(Значение, " / ", НаправлениеПоиска.СКонца); - Если Позиция > 0 Тогда - Значение = СокрЛП(Сред(Значение, Позиция + 3)); - КонецЕсли; - - // "Внешнее исключение (System.Net.WebException): текст" - Позиция = СтрНайти(Значение, "):"); - Если СтрНачинаетсяС(Значение, "Внешнее исключение") И Позиция > 0 Тогда - Значение = СокрЛП(Сред(Значение, Позиция + 2)); - КонецЕсли; - - Возврат Значение; - -КонецФункции - -Функция Содержит(Знач Текст, Знач Подстрока) - Возврат СтрНайти(Текст, Подстрока) > 0; -КонецФункции diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" new file mode 100644 index 0000000..2f77d78 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" @@ -0,0 +1,12 @@ +// Ответ хаба с заданным кодом. + +Перем КодСостояния Экспорт; + +Функция ПолучитьТелоКакДвоичныеДанные() Экспорт + Возврат ПолучитьДвоичныеДанныеИзСтроки("файл пакета"); +КонецФункции + +Процедура Закрыть() Экспорт +КонецПроцедуры + +КодСостояния = 200; diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" new file mode 100644 index 0000000..76b8cad --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -0,0 +1,35 @@ +// Сервер пакетов без сети: отвечает заданным ответом. + +Перем Имя Экспорт; + +Перем Пакеты; +Перем Ответ; + +Процедура ПриСозданииОбъекта(Знач ИмяСервера = "ЗаглушкаХаба") + Имя = ИмяСервера; + Пакеты = Новый Соответствие(); +КонецПроцедуры + +Процедура ОбъявитьПакет(Знач ИмяПакета) Экспорт + Пакеты.Вставить(ИмяПакета, ""); +КонецПроцедуры + +Процедура Отвечать(Знач ОтветХаба) Экспорт + Ответ = ОтветХаба; +КонецПроцедуры + +Функция ПолучитьИмя() Экспорт + Возврат Имя; +КонецФункции + +Функция СерверДоступен() Экспорт + Возврат Истина; +КонецФункции + +Функция ПолучитьПакеты() Экспорт + Возврат Пакеты; +КонецФункции + +Функция ПолучитьРесурс(Знач ИмяРесурса) Экспорт + Возврат Ответ; +КонецФункции diff --git a/tests/network-errors-test.os b/tests/network-errors-test.os index 5aabbd7..3046487 100644 --- a/tests/network-errors-test.os +++ b/tests/network-errors-test.os @@ -2,6 +2,7 @@ #Использовать "../src/core" Перем юТест; +Перем Хаб; Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт @@ -9,131 +10,86 @@ СписокТестов = Новый Массив; - СписокТестов.Добавить("ТестДолжен_УзнатьНеразрешимоеИмяХоста"); - СписокТестов.Добавить("ТестДолжен_УзнатьОтказВСоединении"); - СписокТестов.Добавить("ТестДолжен_УзнатьТаймаут"); - СписокТестов.Добавить("ТестДолжен_УзнатьОшибкуTLS"); - СписокТестов.Добавить("ТестДолжен_ВернутьСутьНеузнаннойОшибки"); - СписокТестов.Добавить("ТестДолжен_СнятьСлужебнуюОбёрткуПлатформы"); - СписокТестов.Добавить("ТестДолжен_НеТрогатьТекстБезОбёртки"); - СписокТестов.Добавить("ТестДолжен_НеЛоматьТелоОтветаХаба"); - СписокТестов.Добавить("ТестДолжен_СобратьСообщениеОНеудачномПодключении"); - СписокТестов.Добавить("ТестДолжен_ЗапомнитьОшибкуСетиСервераПакетов"); + СписокТестов.Добавить("ТестДолжен_СообщитьОтсутствиеФайлаПоКоду404"); + СписокТестов.Добавить("ТестДолжен_СообщитьОтказВДоступеПоКодам401И403"); + СписокТестов.Добавить("ТестДолжен_НазватьИнойКодОтвета"); + СписокТестов.Добавить("ТестДолжен_НазватьАдресСервераБезОтвета"); Возврат СписокТестов; КонецФункции Процедура ПередЗапускомТеста() Экспорт -КонецПроцедуры - -Процедура ПослеЗапускаТеста() Экспорт -КонецПроцедуры - -Процедура ТестДолжен_УзнатьНеразрешимоеИмяХоста() Экспорт - - Ожидаем.Что(ОшибкиСети.Причина(ОбёрнутьКакПлатформа( - "Внешнее исключение (System.Net.WebException): nodename nor servname provided," - + " or not known (lacalhost:3380)")), "macOS").Равно("имя хоста не разрешается"); - - Ожидаем.Что(ОшибкиСети.Причина("The remote name could not be resolved: No such host is known"), "Windows") - .Равно("имя хоста не разрешается"); - - Ожидаем.Что(ОшибкиСети.Причина("Name or service not known"), "Linux") - .Равно("имя хоста не разрешается"); - -КонецПроцедуры - -Процедура ТестДолжен_УзнатьОтказВСоединении() Экспорт - - Ожидаем.Что(ОшибкиСети.Причина(ОбёрнутьКакПлатформа( - "Внешнее исключение (System.Net.WebException): Connection refused [::ffff:127.0.0.1]:9 (127.0.0.1:9)")), - "POSIX").Равно("соединение отклонено — по этому адресу никто не слушает"); - - Ожидаем.Что(ОшибкиСети.Причина("No connection could be made because the target machine actively refused it"), - "Windows").Равно("соединение отклонено — по этому адресу никто не слушает"); - -КонецПроцедуры - -Процедура ТестДолжен_УзнатьТаймаут() Экспорт - - Ожидаем.Что(ОшибкиСети.Причина("The operation has timed out"), "таймаут") - .Равно("истекло время ожидания ответа"); - -КонецПроцедуры - -Процедура ТестДолжен_УзнатьОшибкуTLS() Экспорт - Ожидаем.Что(ОшибкиСети.Причина("The SSL connection could not be established"), "TLS") - .Равно("не удалось установить защищённое соединение (TLS)"); + Хаб = ЗагрузитьЗаглушку("ЗаглушкаСервераПакетов.os"); + Хаб.ОбъявитьПакет("vrunner"); - Ожидаем.Что(ОшибкиСети.Причина("The remote certificate is invalid according to the validation procedure"), - "сертификат").Равно("не удалось установить защищённое соединение (TLS)"); + НастройкиOpm.СброситьНастройки(); + НастройкиOpm.ДобавитьСерверПакетов(Хаб); КонецПроцедуры -Процедура ТестДолжен_ВернутьСутьНеузнаннойОшибки() Экспорт - - Ожидаем.Что(ОшибкиСети.Причина(ОбёрнутьКакПлатформа("Внешнее исключение (System.Exception): что-то своё")), - "неузнанная причина").Равно("что-то своё"); - - Ожидаем.Что(ОшибкиСети.Причина(""), "пустой текст").Равно("соединение не установлено"); - -КонецПроцедуры - -Процедура ТестДолжен_СнятьСлужебнуюОбёрткуПлатформы() Экспорт - - Обёрнутое = "{Модуль /путь/к/СерверПакетов.os / Error in line: 69 /" - + " Внешнее исключение (System.Net.WebException): Connection refused}"; - - Суть = ОшибкиСети.Суть(Обёрнутое); +Процедура ПослеЗапускаТеста() Экспорт - Ожидаем.Что(Суть, "суть без модуля и строки").Равно("Connection refused"); - Ожидаем.Что(СтрНайти(Суть, "Модуль"), "имени модуля быть не должно").Равно(0); - Ожидаем.Что(СтрНайти(Суть, "Error in line"), "номера строки быть не должно").Равно(0); - Ожидаем.Что(СтрНайти(Суть, "System.Net"), "типа исключения быть не должно").Равно(0); + НастройкиOpm.СброситьНастройки(); + ВременныеФайлы.Удалить(); КонецПроцедуры -Процедура ТестДолжен_НеТрогатьТекстБезОбёртки() Экспорт +Процедура ТестДолжен_СообщитьОтсутствиеФайлаПоКоду404() Экспорт - Ожидаем.Что(ОшибкиСети.Суть("Пакет не найден"), "обычное сообщение").Равно("Пакет не найден"); + Ожидаем.Что(ТекстОтказа(404), "404") + .Равно("Ошибка установки пакета vrunner: файл vrunner.ospx в хабе не найден"); КонецПроцедуры -// Тело ответа хаба тоже начинается с фигурной скобки — его переносы строк ломать нельзя. -Процедура ТестДолжен_НеЛоматьТелоОтветаХаба() Экспорт - - Тело = "{" + Символы.ПС + """КодСостояние"": 409," + Символы.ПС + """Сообщение"": ""нельзя""" + Символы.ПС + "}"; +Процедура ТестДолжен_СообщитьОтказВДоступеПоКодам401И403() Экспорт - Ожидаем.Что(ОшибкиСети.Суть(Тело), "JSON остаётся как есть").Равно(Тело); + Ожидаем.Что(ТекстОтказа(401), "401") + .Равно("Ошибка установки пакета vrunner: хаб отказал в доступе к vrunner.ospx (код 401)"); + Ожидаем.Что(ТекстОтказа(403), "403") + .Равно("Ошибка установки пакета vrunner: хаб отказал в доступе к vrunner.ospx (код 403)"); КонецПроцедуры -Процедура ТестДолжен_СобратьСообщениеОНеудачномПодключении() Экспорт - - Сообщение = ОшибкиСети.НеУдалосьПодключиться("http://lacalhost:3380", - ОбёрнутьКакПлатформа("Внешнее исключение (System.Net.WebException): nodename nor servname provided")); +Процедура ТестДолжен_НазватьИнойКодОтвета() Экспорт - Ожидаем.Что(Сообщение, "готовое сообщение") - .Равно("Не удалось подключиться к http://lacalhost:3380: имя хоста не разрешается"); + Ожидаем.Что(ТекстОтказа(500), "500") + .Равно("Ошибка установки пакета vrunner: хаб ответил кодом 500 на запрос vrunner.ospx"); КонецПроцедуры -Процедура ТестДолжен_ЗапомнитьОшибкуСетиСервераПакетов() Экспорт +Процедура ТестДолжен_НазватьАдресСервераБезОтвета() Экспорт // на порту 9 (discard) в тестовом окружении никто не слушает Сервер = Новый СерверПакетов("закрытый", "http://127.0.0.1", "/", "/", 9); Ожидаем.Что(Сервер.ПолучитьРесурс("list.txt"), "ответа нет").Равно(Неопределено); - Ожидаем.Что(Сервер.ПоследняяОшибкаСети, "сообщение с адресом и причиной") - .Равно("Не удалось подключиться к http://127.0.0.1:9: соединение отклонено — по этому адресу никто не слушает"); + Ожидаем.Что(Сервер.ПоследняяОшибкаСети, "сообщение с адресом") + .Равно("Не удалось подключиться к http://127.0.0.1:9"); // недоступный сервер при получении списка пакетов только предупреждает Сервер.ПолучитьПакеты(); КонецПроцедуры -Функция ОбёрнутьКакПлатформа(Знач Текст) - Возврат СтрШаблон("{Модуль /путь/к/Модулю.os / Error in line: 42 / %1}", Текст); +Функция ТекстОтказа(Знач Код) + + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Ответ.КодСостояния = Код; + Хаб.Отвечать(Ответ); + + Попытка + Менеджер = Новый МенеджерПолученияПакетов(); + Менеджер.ПолучитьПакет("vrunner", Неопределено); + Исключение + Возврат ИнформацияОбОшибке().Описание; + КонецПопытки; + + ВызватьИсключение СтрШаблон("Скачивание с кодом %1 должно было провалиться", Код); + +КонецФункции + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); КонецФункции From 5cb5e84c9227d3e1c09eefc01851a73b2eb8168b Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 06:44:23 +0400 Subject: [PATCH 04/17] =?UTF-8?q?=D0=A1=D0=BF=D0=B5=D1=86=D0=B8=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=20@:=20=D1=82=D0=BE=D1=87=D0=BD=D0=B0=D1=8F=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B8=D1=8F,=20=D1=82=D0=B5=D0=B3=20=D0=B8?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=B2=D1=8B=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20=D1=83=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D1=8F=D1=82=20=D1=85=D0=B0=D0=B1=D1=83=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=BE=D0=BC=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Имя файла пакета в адресе прежнее (<пакет>-<спецификация>.ospx), сегменты пути и значение spec кодируются одной функцией СерверПакетов. На 404 по тегу или выражению версии opm говорит, что их разрешает только хаб, принимающий spec. Файловый сервер при названной спецификации не подбирает старшую версию. Справка PACKAGE команд install, download и update — одной копией в ПараметрыПриложенияOpm. --- README.md | 16 ++ ...60\320\275\320\264\320\260Opm_Download.os" | 2 +- ...260\320\275\320\264\320\260Opm_Install.os" | 2 +- ...\260\320\275\320\264\320\260Opm_Update.os" | 2 +- ...266\320\265\320\275\320\270\321\217Opm.os" | 10 + ...20\272\320\265\321\202\320\276\320\262.os" | 18 +- ...20\272\320\265\321\202\320\276\320\262.os" | 51 ++++- ...20\236\321\202\320\262\320\265\321\202.os" | 4 +- ...20\272\320\265\321\202\320\276\320\262.os" | 5 +- ...20\265\321\202\320\260\320\274\320\270.os" | 16 +- ...20\272\320\265\321\202\320\276\320\262.os" | 11 +- tests/package-spec-test.os | 183 ++++++++++++++++++ 12 files changed, 296 insertions(+), 24 deletions(-) create mode 100644 tests/package-spec-test.os diff --git a/README.md b/README.md index 0af47ef..4044233 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,22 @@ OneScript Package Manager Справка из википедии: [Система управления пакетами](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D0%BF%D0%B0%D0%BA%D0%B5%D1%82%D0%B0%D0%BC%D0%B8) +# Версия пакета после @ + +`install`, `update` и `download` принимают после `@` точную версию, имя тега или выражение версии: + +```sh +opm install vrunner@1.0.0 # точная версия +opm install vrunner@stable # версия под тегом +opm install vrunner@^1.4 # старшая версия, подходящая под выражение +opm install vrunner # версию выбирает хаб +``` + +Спецификация уходит хабу параметром `?spec=`, имя файла в адресе прежнее — `<пакет>-<спецификация>.ospx`. +Тег и выражение версии разрешает только хаб, принимающий `spec`; хаб без него отвечает 404, и opm так и говорит. +Точная версия ставится с любым хабом. Файловый сервер (`file://`) отдаёт только файл названной версии +и не подставляет вместо неё старшую. + # Ошибки связи Если хаб ответил, но файла не отдал, причина называется по коду ответа: 404 — файл не найден, diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Download.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Download.os" index 57e0253..9593464 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Download.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Download.os" @@ -21,7 +21,7 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. Чтобы установить конкретную версию, используйте ИмяПакета@ВерсияПакета") + КомандаПриложения.Аргумент("PACKAGE", "", ПараметрыПриложенияOpm.ОписаниеАргументаПакет()) .ТМассивСтрок() .Обязательный(Ложь); diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" index dad2baf..2d6e5c4 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" @@ -23,7 +23,7 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. Чтобы установить конкретную версию, используйте ИмяПакета@ВерсияПакета") + КомандаПриложения.Аргумент("PACKAGE", "", ПараметрыПриложенияOpm.ОписаниеАргументаПакет()) .ТМассивСтрок() .Обязательный(Ложь); diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Update.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Update.os" index 10244f4..b204dc9 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Update.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Update.os" @@ -22,7 +22,7 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. Чтобы установить конкретную версию, используйте ИмяПакета@ВерсияПакета") + КомандаПриложения.Аргумент("PACKAGE", "", ПараметрыПриложенияOpm.ОписаниеАргументаПакет()) .ТМассивСтрок() .Обязательный(Ложь); diff --git "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" index 034828c..6e04508 100644 --- "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" +++ "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" @@ -15,6 +15,16 @@ Возврат "oscript.app.opm"; КонецФункции +// Описание аргумента PACKAGE команд install, download и update. +// +// Возвращаемое значение: +// Строка - текст справки. +// +Функция ОписаниеАргументаПакет() Экспорт + Возврат "Имя пакета в хабе. После @ можно указать точную версию (пакет@1.0.0), + | имя тега (пакет@stable) или выражение версии (пакет@^1.4)."; +КонецФункции + Процедура НастроитьOpm() Экспорт НастройкиOpmИзФайлов = СобратьНастройкиИзФайлов(); diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index a709db3..3f68386 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -10,7 +10,7 @@ КонецПроцедуры -Функция ПолучитьПакет(Знач ИмяПакета, Знач ВерсияПакета, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт +Функция ПолучитьПакет(Знач ИмяПакета, Знач Спецификация, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт ОбновитьИндексыКешейПакетов(); @@ -23,8 +23,8 @@ ИмяПакета = ОпределитьИмяАрхива(ИмяПакета); - Если ВерсияПакета <> Неопределено Тогда - ФайлПакета = ИмяПакета + "-" + ВерсияПакета + ".ospx"; + Если Спецификация <> Неопределено Тогда + ФайлПакета = ИмяПакета + "-" + Спецификация + ".ospx"; Иначе ФайлПакета = ИмяПакета + ".ospx"; КонецЕсли; @@ -39,7 +39,7 @@ ПереченьСерверов = ИндексКешаПакетов[ИмяПакета]; - Полученное = ЗапроситьПакет(ПереченьСерверов, ИмяСервера, ИмяРесурса); + Полученное = ЗапроситьПакет(ПереченьСерверов, ИмяСервера, ИмяРесурса, Спецификация); Ответ = Полученное.Ответ; Если Не Ответ = Неопределено Тогда @@ -76,7 +76,7 @@ // Возвращаемое значение: // Структура - Ответ (HTTPОтвет, Неопределено), КодСостояния (Число, 0 — ответа не было), // ОшибкаСети (Строка - сообщение о неудачном подключении к единственному названному серверу). -Функция ЗапроситьПакет(Знач ПереченьСерверов, Знач ИмяСервера, Знач ИмяРесурса) +Функция ЗапроситьПакет(Знач ПереченьСерверов, Знач ИмяСервера, Знач ИмяРесурса, Знач Спецификация) ПакетУспешноПолучен = Ложь; ОтветСервера = Неопределено; @@ -87,7 +87,7 @@ Если ЗначениеЗаполнено(ИмяСервера) Тогда Сервер = ИндексСерверовПакетов[ИмяСервера]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, КодСостояния); + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния); Если ОтветСервера = Неопределено И ТипЗнч(Сервер) = Тип("СерверПакетов") Тогда ОшибкаСети = Сервер.ПоследняяОшибкаСети; КонецЕсли; @@ -98,7 +98,7 @@ Для Каждого ДоступныйСервер Из ПереченьСерверов Цикл Сервер = ИндексСерверовПакетов[ДоступныйСервер.Ключ]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, КодСостояния); + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния); Если ОтветСервера <> Неопределено Тогда Прервать; @@ -119,12 +119,12 @@ КонецФункции -Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса, КодСостояния) +Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса, Знач Спецификация, КодСостояния) ОтветСервера = Неопределено; Если Сервер.СерверДоступен() Тогда - ОтветСервера = Сервер.ПолучитьРесурс(ИмяРесурса); + ОтветСервера = Сервер.ПолучитьРесурс(ИмяРесурса, Спецификация); Если ОтветСервера <> Неопределено Тогда КодСостояния = ОтветСервера.КодСостояния; diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 66c45e1..ab255e8 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -57,12 +57,20 @@ КонецФункции -// ИмяРесурса - имя файла относительно "Сервер/ПутьВХранилище" -// Возвращает HttpОтвет или Неопределено, если запрос вернул исключение. -Функция ПолучитьРесурс(Знач ИмяРесурса) Экспорт +// Получает ресурс с сервера. +// +// Параметры: +// ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьНаСервере". +// Спецификация - Строка, Неопределено - строка после @, уходит параметром запроса spec; +// пустая параметра не добавляет. +// +// Возвращаемое значение: +// HTTPОтвет - ответ сервера; Неопределено, если запрос завершился исключением. +// +Функция ПолучитьРесурс(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт Соединение = ИнициализироватьСоединение(); - Ресурс = ПутьНаСервере + ИмяРесурса; + Ресурс = АдресРесурса(ИмяРесурса, Спецификация); Запрос = Новый HTTPЗапрос(Ресурс); ДобавитьЗаголовки(Запрос); ПоследняяОшибкаСети = ""; @@ -81,6 +89,41 @@ КонецФункции +// Адрес ресурса в запросе к серверу: путь с закодированными сегментами и строка запроса. +// +// Параметры: +// ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьНаСервере". +// Спецификация - Строка, Неопределено - строка после @; пустая параметра spec не добавляет. +// +// Возвращаемое значение: +// Строка - путь и, если есть параметры, строка запроса. +// +Функция АдресРесурса(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт + + Сегменты = СтрРазделить(ИмяРесурса, "/"); + Для Индекс = 0 По Сегменты.ВГраница() Цикл + Сегменты[Индекс] = ЧастьАдреса(Сегменты[Индекс]); + КонецЦикла; + + ПараметрыЗапроса = Новый Массив; + Если ЗначениеЗаполнено(Спецификация) Тогда + ПараметрыЗапроса.Добавить("spec=" + ЧастьАдреса(Спецификация)); + КонецЕсли; + + Адрес = ПутьНаСервере + СтрСоединить(Сегменты, "/"); + Если ПараметрыЗапроса.Количество() = 0 Тогда + Возврат Адрес; + КонецЕсли; + + Возврат Адрес + "?" + СтрСоединить(ПараметрыЗапроса, "&"); + +КонецФункции + +// Платформа оставляет плюс в адресе как есть, а в строке запроса он читается пробелом. +Функция ЧастьАдреса(Знач Значение) + Возврат КодироватьСтроку(Значение, СпособКодированияСтроки.КодировкаURL); +КонецФункции + // Адрес сервера в сообщениях пользователю: порт указывается, только если он не стандартный. Функция АдресДляСообщений() diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" index 9df20eb..4a5946b 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" @@ -4,7 +4,7 @@ Перем КодСостояния Экспорт; Перем ИмяФайла; -Процедура ПриСозданииОбъекта(Знач ПутьКФайлу) +Процедура ПриСозданииОбъекта(Знач ПутьКФайлу, Знач ПодбиратьВерсию = Истина) ИмяФайла = ПутьКФайлу; @@ -15,7 +15,7 @@ КодСостояния = 404; - Если СтрЗаканчиваетсяНа(ПутьКФайлу, ".ospx") Тогда + Если ПодбиратьВерсию И СтрЗаканчиваетсяНа(ПутьКФайлу, ".ospx") Тогда ПодобратьФайлПакета(Файл.Путь); КонецЕсли; diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index fceeaea..e7c26d8 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -33,17 +33,18 @@ // // Параметры: // ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьВХранилище" +// Спецификация - Строка, Неопределено - строка после @; названная запрещает подбор другой версии пакета // // Возвращаемое значение: // ФайловыйСерверОтвет - Если файл ресурса найден // Неопределено - Если файл ресурса не найден или возникла ошибка обращения к файлам -Функция ПолучитьРесурс(Знач ИмяРесурса) Экспорт +Функция ПолучитьРесурс(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт ПутьКРесурсу = ОбъединитьПути(КорневойКаталогПоиска, ИмяРесурса); Попытка - Возврат Новый ФайловыйСерверОтвет(ПутьКРесурсу); + Возврат Новый ФайловыйСерверОтвет(ПутьКРесурсу, Спецификация = Неопределено); Исключение diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" index bf36600..eda4708 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" @@ -226,10 +226,22 @@ КонецПроцедуры -Функция ПолучитьПакет(Знач ИмяПакета, Знач ВерсияПакета, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт +// Скачивает файл пакета с сервера пакетов. +// +// Параметры: +// ИмяПакета - Строка - имя пакета. +// Спецификация - Строка, Неопределено - строка после @: точный номер версии, имя тега или выражение версии; +// Неопределено — версию выбирает сервер. +// ПутьКФайлуПакета - Строка - куда положить файл; пустая строка — во временный файл. +// ИмяСервера - Строка - имя сервера пакетов; пустая строка — перебор всех серверов. +// +// Возвращаемое значение: +// Строка - путь к скачанному файлу пакета. +// +Функция ПолучитьПакет(Знач ИмяПакета, Знач Спецификация, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт МенеджерПолучения = ПолучитьМенеджерПолученияПакетов(); - ПутьКФайлуПакета = МенеджерПолучения.ПолучитьПакет(ИмяПакета, ВерсияПакета, ПутьКФайлуПакета, ИмяСервера); + ПутьКФайлуПакета = МенеджерПолучения.ПолучитьПакет(ИмяПакета, Спецификация, ПутьКФайлуПакета, ИмяСервера); Возврат ПутьКФайлуПакета; diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 76b8cad..3a13529 100644 --- "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -1,6 +1,7 @@ -// Сервер пакетов без сети: отвечает заданным ответом. +// Сервер пакетов без сети: запоминает запросы и отвечает заданным ответом. Перем Имя Экспорт; +Перем Запросы Экспорт; Перем Пакеты; Перем Ответ; @@ -8,6 +9,7 @@ Процедура ПриСозданииОбъекта(Знач ИмяСервера = "ЗаглушкаХаба") Имя = ИмяСервера; Пакеты = Новый Соответствие(); + Запросы = Новый Массив(); КонецПроцедуры Процедура ОбъявитьПакет(Знач ИмяПакета) Экспорт @@ -18,6 +20,10 @@ Ответ = ОтветХаба; КонецПроцедуры +Функция ПоследнийЗапрос() Экспорт + Возврат Запросы[Запросы.ВГраница()]; +КонецФункции + Функция ПолучитьИмя() Экспорт Возврат Имя; КонецФункции @@ -30,6 +36,7 @@ Возврат Пакеты; КонецФункции -Функция ПолучитьРесурс(Знач ИмяРесурса) Экспорт +Функция ПолучитьРесурс(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт + Запросы.Добавить(Новый Структура("ИмяРесурса, Спецификация", ИмяРесурса, Спецификация)); Возврат Ответ; КонецФункции diff --git a/tests/package-spec-test.os b/tests/package-spec-test.os new file mode 100644 index 0000000..6f4ca53 --- /dev/null +++ b/tests/package-spec-test.os @@ -0,0 +1,183 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; +Перем Хаб; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ЗапроситьФайлВерсииИСпецификациюОтдельно"); + СписокТестов.Добавить("ТестДолжен_ЗапроситьФайлБезСпецификацииКогдаЕёНеНазвали"); + СписокТестов.Добавить("ТестДолжен_СобратьАдресЗапросаСоСпецификацией"); + СписокТестов.Добавить("ТестДолжен_СобратьАдресЗапросаБезСпецификации"); + СписокТестов.Добавить("ТестДолжен_ЗакодироватьСпецификациюВАдресе"); + СписокТестов.Добавить("ТестДолжен_СохранитьПрежнийОтказНаТочнуюВерсию"); + СписокТестов.Добавить("ТестДолжен_ЗапретитьФайловомуСерверуПодборПодСпецификацию"); + СписокТестов.Добавить("ТестДолжен_ОставитьФайловомуСерверуПодборБезСпецификации"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + Хаб = ЗагрузитьЗаглушку("ЗаглушкаСервераПакетов.os"); + Хаб.ОбъявитьПакет("vrunner"); + Хаб.Отвечать(ОтветХаба(200)); + + НастройкиOpm.СброситьНастройки(); + НастройкиOpm.ДобавитьСерверПакетов(Хаб); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + НастройкиOpm.СброситьНастройки(); + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапроситьФайлВерсииИСпецификациюОтдельно() Экспорт + + Для Каждого Спецификация Из СтрРазделить("stable,^1.4,~1.4,1.4", ",") Цикл + + Скачать("vrunner", Спецификация); + + Ожидаем.Что(Хаб.ПоследнийЗапрос().ИмяРесурса, "имя ресурса для " + Спецификация) + .Равно(СтрШаблон("vrunner/vrunner-%1.ospx", Спецификация)); + Ожидаем.Что(Хаб.ПоследнийЗапрос().Спецификация, "спецификация").Равно(Спецификация); + + КонецЦикла; + +КонецПроцедуры + +Процедура ТестДолжен_ЗапроситьФайлБезСпецификацииКогдаЕёНеНазвали() Экспорт + + Скачать("vrunner", Неопределено); + + Ожидаем.Что(Хаб.ПоследнийЗапрос().ИмяРесурса, "имя ресурса").Равно("vrunner/vrunner.ospx"); + Ожидаем.Что(Хаб.ПоследнийЗапрос().Спецификация, "спецификация").Равно(Неопределено); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьАдресЗапросаСоСпецификацией() Экспорт + + Сервер = СерверПробы(); + + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-stable.ospx", "stable"), "тег") + .Равно("/download/vrunner/vrunner-stable.ospx?spec=stable"); + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-1.0.0.ospx", "1.0.0"), "точная версия") + .Равно("/download/vrunner/vrunner-1.0.0.ospx?spec=1.0.0"); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьАдресЗапросаБезСпецификации() Экспорт + + Сервер = СерверПробы(); + + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner.ospx"), "файл пакета").Равно("/download/vrunner/vrunner.ospx"); + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-.ospx", ""), "пустая спецификация") + .Равно("/download/vrunner/vrunner-.ospx"); + Ожидаем.Что(Сервер.АдресРесурса("list.txt"), "список пакетов").Равно("/download/list.txt"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗакодироватьСпецификациюВАдресе() Экспорт + + Сервер = СерверПробы(); + + Для Каждого Образец Из СтрРазделить("^1.4|~1.4|1.0.0-rc.1+build|>=1.0 <2.0|feature/foo", "|") Цикл + + Адрес = Сервер.АдресРесурса("vrunner/vrunner.ospx", Образец); + Параметр = Сред(Адрес, СтрНайти(Адрес, "?spec=") + СтрДлина("?spec=")); + + Ожидаем.Что(СтрНайти(Параметр, "+") + СтрНайти(Параметр, " ") + СтрНайти(Параметр, "/"), + "служебные символы закодированы в " + Адрес).Равно(0); + Ожидаем.Что(РаскодироватьСтроку(Параметр, СпособКодированияСтроки.КодировкаURL), "значение в " + Адрес) + .Равно(Образец); + + КонецЦикла; + + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-^1.4.ospx", "^1.4"), "имя файла") + .Равно("/download/vrunner/vrunner-%5E1.4.ospx?spec=%5E1.4"); + +КонецПроцедуры + +Процедура ТестДолжен_СохранитьПрежнийОтказНаТочнуюВерсию() Экспорт + + Хаб.Отвечать(ОтветХаба(404)); + + Ожидаем.Что(ТекстОтказа("vrunner", "1.4"), "точная версия").Содержит("файл vrunner-1.4.ospx в хабе не найден"); + Ожидаем.Что(ТекстОтказа("vrunner", Неопределено), "без спецификации") + .Содержит("файл vrunner.ospx в хабе не найден"); + Ожидаем.Что(ТекстОтказа("vrunner", ""), "пустая спецификация").Содержит("файл vrunner-.ospx в хабе не найден"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапретитьФайловомуСерверуПодборПодСпецификацию() Экспорт + + Сервер = Новый ФайловыйСерверПакетов("зеркалоПробы", "file://", КаталогФайловогоСервера()); + + Ожидаем.Что(Сервер.ПолучитьРесурс("vrunner/vrunner-stable.ospx", "stable").КодСостояния, "тег").Равно(404); + Ожидаем.Что(Сервер.ПолучитьРесурс("vrunner/vrunner-0.0.1.ospx", "0.0.1").КодСостояния, "версии нет") + .Равно(404); + Ожидаем.Что(Сервер.ПолучитьРесурс("vrunner/vrunner-9.9.9.ospx", "9.9.9").КодСостояния, "версия есть") + .Равно(200); + +КонецПроцедуры + +Процедура ТестДолжен_ОставитьФайловомуСерверуПодборБезСпецификации() Экспорт + + Сервер = Новый ФайловыйСерверПакетов("зеркалоПробы", "file://", КаталогФайловогоСервера()); + + Ожидаем.Что(Сервер.ПолучитьРесурс("vrunner/vrunner.ospx").КодСостояния, "подбор старшей версии").Равно(200); + +КонецПроцедуры + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции + +Функция ОтветХаба(Знач Код) + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Ответ.КодСостояния = Код; + Возврат Ответ; +КонецФункции + +Функция СерверПробы() + Возврат Новый СерверПакетов("ХабПробы", "http://hub.example.com", "/download/", "/push", 80); +КонецФункции + +Функция Скачать(Знач ИмяПакета, Знач Спецификация) + Возврат Новый МенеджерПолученияПакетов().ПолучитьПакет(ИмяПакета, Спецификация); +КонецФункции + +Функция ТекстОтказа(Знач ИмяПакета, Знач Спецификация) + + Попытка + Скачать(ИмяПакета, Спецификация); + Исключение + Возврат ОписаниеОшибки(); + КонецПопытки; + + ВызватьИсключение СтрШаблон("Скачивание <%1@%2> должно было провалиться", ИмяПакета, Спецификация); + +КонецФункции + +// В каталоге одна версия 9.9.9: подбор выдал бы её за любую запрошенную. +Функция КаталогФайловогоСервера() + + Каталог = юТест.ИмяВременногоФайла(); + СоздатьКаталог(ОбъединитьПути(Каталог, "vrunner")); + + Запись = Новый ЗаписьТекста(ОбъединитьПути(Каталог, "vrunner", "vrunner-9.9.9.ospx")); + Запись.Записать("файл пакета"); + Запись.Закрыть(); + + Возврат Каталог; + +КонецФункции From 4fa8404006452e106e95d680904fb797012b5e8f Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 06:49:04 +0400 Subject: [PATCH 05/17] =?UTF-8?q?=D0=9F=D0=B0=D1=80=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=20engine:=20=D0=BA=20=D0=B7=D0=B0=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D1=83=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20OneScript,=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D1=87=D0=B8=D0=BD=D0=B0=20=D0=BE=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=D0=B0=20=D0=B8=D0=B7=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=B0=20=D1=85=D0=B0=D0=B1=D0=B0=20=D0=BF=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++ ...20\272\320\265\321\202\320\276\320\262.os" | 17 +++++-- ...20\272\320\265\321\202\320\276\320\262.os" | 4 ++ ...20\236\321\202\320\262\320\265\321\202.os" | 4 ++ ...20\260\320\272\320\265\321\202\320\260.os" | 12 ++++- ...20\260\320\245\320\260\320\261\320\260.os" | 8 ++- tests/package-spec-test.os | 49 ++++++++++++++++--- 7 files changed, 83 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4044233..5a69f37 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ opm install vrunner # версию выбирает хаб Точная версия ставится с любым хабом. Файловый сервер (`file://`) отдаёт только файл названной версии и не подставляет вместо неё старшую. +К запросу файла пакета opm добавляет параметр `engine` — версию установленного OneScript. Хаб, который его понимает, +выбирает версию под этот движок там, где версию выбирает он (без `@`, тег, выражение); точная версия отдаётся как есть. +Если хаб отказал и объяснил причину в ответе, opm показывает её. + # Ошибки связи Если хаб ответил, но файла не отдал, причина называется по коду ответа: 404 — файл не найден, diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 3f68386..67134b2 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -59,7 +59,11 @@ Функция ПричинаНеудачи(Знач ФайлПакета, Знач Полученное) КодСостояния = Полученное.КодСостояния; - Если КодСостояния = 404 Тогда + Если СтрДлина(Полученное.ТелоОтвета) > 300 Тогда + Возврат Лев(Полученное.ТелоОтвета, 300) + "..."; + ИначеЕсли ЗначениеЗаполнено(Полученное.ТелоОтвета) Тогда + Возврат Полученное.ТелоОтвета; + ИначеЕсли КодСостояния = 404 Тогда Возврат СтрШаблон("файл %1 в хабе не найден", ФайлПакета); ИначеЕсли КодСостояния = 401 ИЛИ КодСостояния = 403 Тогда Возврат СтрШаблон("хаб отказал в доступе к %1 (код %2)", ФайлПакета, Формат(КодСостояния, "ЧГ=0")); @@ -75,19 +79,21 @@ // Возвращаемое значение: // Структура - Ответ (HTTPОтвет, Неопределено), КодСостояния (Число, 0 — ответа не было), +// ТелоОтвета (Строка - текст ответа с кодом не 200 без пробелов по краям), // ОшибкаСети (Строка - сообщение о неудачном подключении к единственному названному серверу). Функция ЗапроситьПакет(Знач ПереченьСерверов, Знач ИмяСервера, Знач ИмяРесурса, Знач Спецификация) ПакетУспешноПолучен = Ложь; ОтветСервера = Неопределено; КодСостояния = 0; + ТелоОтвета = ""; ОшибкаСети = ""; // Если указан нужный сервер, то только его и будем использовать Если ЗначениеЗаполнено(ИмяСервера) Тогда Сервер = ИндексСерверовПакетов[ИмяСервера]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния); + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния, ТелоОтвета); Если ОтветСервера = Неопределено И ТипЗнч(Сервер) = Тип("СерверПакетов") Тогда ОшибкаСети = Сервер.ПоследняяОшибкаСети; КонецЕсли; @@ -98,7 +104,7 @@ Для Каждого ДоступныйСервер Из ПереченьСерверов Цикл Сервер = ИндексСерверовПакетов[ДоступныйСервер.Ключ]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния); + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния, ТелоОтвета); Если ОтветСервера <> Неопределено Тогда Прервать; @@ -115,11 +121,11 @@ КонецЕсли; - Возврат Новый Структура("Ответ, КодСостояния, ОшибкаСети", ОтветСервера, КодСостояния, ОшибкаСети); + Возврат Новый Структура("Ответ, КодСостояния, ТелоОтвета, ОшибкаСети", ОтветСервера, КодСостояния, ТелоОтвета, ОшибкаСети); КонецФункции -Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса, Знач Спецификация, КодСостояния) +Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса, Знач Спецификация, КодСостояния, ТелоОтвета) ОтветСервера = Неопределено; Если Сервер.СерверДоступен() Тогда @@ -135,6 +141,7 @@ КонецЕсли; + ТелоОтвета = СокрЛП(ОтветСервера.ПолучитьТелоКакСтроку()); ОтветСервера.Закрыть(); Лог.Отладка("Хаб %1 ответил кодом %2 на запрос %3", diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index ab255e8..92612ef 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -94,6 +94,7 @@ // Параметры: // ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьНаСервере". // Спецификация - Строка, Неопределено - строка после @; пустая параметра spec не добавляет. +// К файлу пакета добавляется параметр engine - версия установленного движка. // // Возвращаемое значение: // Строка - путь и, если есть параметры, строка запроса. @@ -109,6 +110,9 @@ Если ЗначениеЗаполнено(Спецификация) Тогда ПараметрыЗапроса.Добавить("spec=" + ЧастьАдреса(Спецификация)); КонецЕсли; + Если СтрЗаканчиваетсяНа(ИмяРесурса, ".ospx") Тогда + ПараметрыЗапроса.Добавить("engine=" + ЧастьАдреса(РаботаСОписаниемПакета.ВерсияДвижка())); + КонецЕсли; Адрес = ПутьНаСервере + СтрСоединить(Сегменты, "/"); Если ПараметрыЗапроса.Количество() = 0 Тогда diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" index 4a5946b..dc2a45c 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" @@ -27,6 +27,10 @@ Возврат Новый ДвоичныеДанные(ИмяФайла); КонецФункции +Функция ПолучитьТелоКакСтроку() Экспорт + Возврат ""; +КонецФункции + Процедура Закрыть() Экспорт // для совместимости контракта КонецПроцедуры diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274\320\237\320\260\320\272\320\265\321\202\320\260.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274\320\237\320\260\320\272\320\265\321\202\320\260.os" index 728349b..5cda1a6 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274\320\237\320\260\320\272\320\265\321\202\320\260.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274\320\237\320\260\320\272\320\265\321\202\320\260.os" @@ -59,8 +59,7 @@ ИмяПакета = Свойства.Имя; ТребуемаяВерсияСреды = Свойства.ВерсияСреды; - СистемнаяИнформация = Новый СистемнаяИнформация; - ВерсияСреды = СистемнаяИнформация.Версия; + ВерсияСреды = ВерсияДвижка(); Лог().Отладка("ПроверитьВерсиюСреды: Перед вызовом СравнитьВерсии(ЭтаВерсия = <%1>, БольшеЧемВерсия = <%2>)", ТребуемаяВерсияСреды, ВерсияСреды); Попытка @@ -86,6 +85,15 @@ КонецПроцедуры +// Версия установленного движка OneScript. +// +// Возвращаемое значение: +// Строка - версия, например 2.0.0. +// +Функция ВерсияДвижка() Экспорт + Возврат Новый СистемнаяИнформация().Версия; +КонецФункции + Функция Лог() Если Лог = Неопределено Тогда Лог = Логирование.ПолучитьЛог("oscript.app.opm"); diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" index 2f77d78..27b3c72 100644 --- "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" @@ -1,12 +1,18 @@ -// Ответ хаба с заданным кодом. +// Ответ хаба с заданным кодом и текстом. Перем КодСостояния Экспорт; +Перем Тело Экспорт; Функция ПолучитьТелоКакДвоичныеДанные() Экспорт Возврат ПолучитьДвоичныеДанныеИзСтроки("файл пакета"); КонецФункции +Функция ПолучитьТелоКакСтроку() Экспорт + Возврат Тело; +КонецФункции + Процедура Закрыть() Экспорт КонецПроцедуры КодСостояния = 200; +Тело = ""; diff --git a/tests/package-spec-test.os b/tests/package-spec-test.os index 6f4ca53..05a3223 100644 --- a/tests/package-spec-test.os +++ b/tests/package-spec-test.os @@ -15,6 +15,8 @@ СписокТестов.Добавить("ТестДолжен_СобратьАдресЗапросаСоСпецификацией"); СписокТестов.Добавить("ТестДолжен_СобратьАдресЗапросаБезСпецификации"); СписокТестов.Добавить("ТестДолжен_ЗакодироватьСпецификациюВАдресе"); + СписокТестов.Добавить("ТестДолжен_ПередатьВерсиюДвижкаТолькоВЗапросеФайлаПакета"); + СписокТестов.Добавить("ТестДолжен_ПоказатьПричинуОтказаИзОтветаХаба"); СписокТестов.Добавить("ТестДолжен_СохранитьПрежнийОтказНаТочнуюВерсию"); СписокТестов.Добавить("ТестДолжен_ЗапретитьФайловомуСерверуПодборПодСпецификацию"); СписокТестов.Добавить("ТестДолжен_ОставитьФайловомуСерверуПодборБезСпецификации"); @@ -69,9 +71,9 @@ Сервер = СерверПробы(); Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-stable.ospx", "stable"), "тег") - .Равно("/download/vrunner/vrunner-stable.ospx?spec=stable"); + .Равно("/download/vrunner/vrunner-stable.ospx?spec=stable&" + Движок()); Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-1.0.0.ospx", "1.0.0"), "точная версия") - .Равно("/download/vrunner/vrunner-1.0.0.ospx?spec=1.0.0"); + .Равно("/download/vrunner/vrunner-1.0.0.ospx?spec=1.0.0&" + Движок()); КонецПроцедуры @@ -79,9 +81,9 @@ Сервер = СерверПробы(); - Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner.ospx"), "файл пакета").Равно("/download/vrunner/vrunner.ospx"); + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner.ospx"), "файл пакета").Равно("/download/vrunner/vrunner.ospx?" + Движок()); Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-.ospx", ""), "пустая спецификация") - .Равно("/download/vrunner/vrunner-.ospx"); + .Равно("/download/vrunner/vrunner-.ospx?" + Движок()); Ожидаем.Что(Сервер.АдресРесурса("list.txt"), "список пакетов").Равно("/download/list.txt"); КонецПроцедуры @@ -93,7 +95,7 @@ Для Каждого Образец Из СтрРазделить("^1.4|~1.4|1.0.0-rc.1+build|>=1.0 <2.0|feature/foo", "|") Цикл Адрес = Сервер.АдресРесурса("vrunner/vrunner.ospx", Образец); - Параметр = Сред(Адрес, СтрНайти(Адрес, "?spec=") + СтрДлина("?spec=")); + Параметр = СтрРазделить(Сред(Адрес, СтрНайти(Адрес, "?spec=") + СтрДлина("?spec=")), "&")[0]; Ожидаем.Что(СтрНайти(Параметр, "+") + СтрНайти(Параметр, " ") + СтрНайти(Параметр, "/"), "служебные символы закодированы в " + Адрес).Равно(0); @@ -103,7 +105,35 @@ КонецЦикла; Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-^1.4.ospx", "^1.4"), "имя файла") - .Равно("/download/vrunner/vrunner-%5E1.4.ospx?spec=%5E1.4"); + .Равно("/download/vrunner/vrunner-%5E1.4.ospx?spec=%5E1.4&" + Движок()); + +КонецПроцедуры + +Процедура ТестДолжен_ПередатьВерсиюДвижкаТолькоВЗапросеФайлаПакета() Экспорт + + Сервер = СерверПробы(); + ВерсияДвижка = Новый СистемнаяИнформация().Версия; + + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-stable.ospx", "stable"), "файл по тегу") + .Содержит("&engine=" + ВерсияДвижка); + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner.ospx"), "файл без спецификации") + .Содержит("?engine=" + ВерсияДвижка); + Ожидаем.Что(Сервер.АдресРесурса("list.txt"), "список пакетов").Не_().Содержит("engine="); + +КонецПроцедуры + +Процедура ТестДолжен_ПоказатьПричинуОтказаИзОтветаХаба() Экспорт + + Хаб.Отвечать(ОтветХаба(404, " нет версии под движок 1.9.0" + Символы.ПС)); + Ожидаем.Что(ТекстОтказа("vrunner", "stable"), "404 с причиной") + .Содержит("Ошибка установки пакета vrunner: нет версии под движок 1.9.0"); + + Хаб.Отвечать(ОтветХаба(422, "bad request: engine")); + Ожидаем.Что(ТекстОтказа("vrunner", Неопределено), "422").Содержит("Ошибка установки пакета vrunner: bad request: engine"); + + Длинное = СтрСоединить(Новый Массив(400), "x") + "конец"; + Хаб.Отвечать(ОтветХаба(404, Длинное)); + Ожидаем.Что(ТекстОтказа("vrunner", Неопределено), "длинный ответ").Не_().Содержит("конец"); КонецПроцедуры @@ -142,12 +172,17 @@ Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); КонецФункции -Функция ОтветХаба(Знач Код) +Функция ОтветХаба(Знач Код, Знач Тело = "") Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); Ответ.КодСостояния = Код; + Ответ.Тело = Тело; Возврат Ответ; КонецФункции +Функция Движок() + Возврат "engine=" + Новый СистемнаяИнформация().Версия; +КонецФункции + Функция СерверПробы() Возврат Новый СерверПакетов("ХабПробы", "http://hub.example.com", "/download/", "/push", 80); КонецФункции From 36e96df3f56ad9fd77c0a88f666aae49a01fa9c9 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 09:40:48 +0400 Subject: [PATCH 06/17] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=B0=20engine:=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B2=20=D0=BE=D0=B6=D0=B8=D0=B4=D0=B0=D0=B5=D0=BC?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D0=BE=D0=B4=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0,=20=D0=BA=D0=B0=D0=BA=20=D0=B2=20=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=20(=D0=BF=D0=BB=D1=8E=D1=81=20=D1=81=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=BA=D0=B8=20dev-=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/package-spec-test.os | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/package-spec-test.os b/tests/package-spec-test.os index 05a3223..8e5b047 100644 --- a/tests/package-spec-test.os +++ b/tests/package-spec-test.os @@ -112,12 +112,11 @@ Процедура ТестДолжен_ПередатьВерсиюДвижкаТолькоВЗапросеФайлаПакета() Экспорт Сервер = СерверПробы(); - ВерсияДвижка = Новый СистемнаяИнформация().Версия; Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-stable.ospx", "stable"), "файл по тегу") - .Содержит("&engine=" + ВерсияДвижка); + .Содержит("&" + Движок()); Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner.ospx"), "файл без спецификации") - .Содержит("?engine=" + ВерсияДвижка); + .Содержит("?" + Движок()); Ожидаем.Что(Сервер.АдресРесурса("list.txt"), "список пакетов").Не_().Содержит("engine="); КонецПроцедуры @@ -179,8 +178,9 @@ Возврат Ответ; КонецФункции +// Версия движка в строке запроса закодирована: плюс сборки иначе читается пробелом. Функция Движок() - Возврат "engine=" + Новый СистемнаяИнформация().Версия; + Возврат "engine=" + КодироватьСтроку(Новый СистемнаяИнформация().Версия, СпособКодированияСтроки.КодировкаURL); КонецФункции Функция СерверПробы() From 0a26c7a4b06542d9064734cb40f2ae4cf55bb958 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 06:55:37 +0400 Subject: [PATCH 07/17] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=81=D1=83=D0=BC=D0=BC=D0=B0?= =?UTF-8?q?=20=D1=81=D0=BA=D0=B0=D1=87=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=B0:=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=20=D1=81=D0=B2=D0=B5=D1=80=D1=8F=D0=B5=D1=82=D1=81=D1=8F?= =?UTF-8?q?=20=D1=81=20sha256=20=D0=B8=D0=B7=20=D0=B7=D0=B0=D0=B3=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D0=BA=D0=B0=20X-Checksum-Sha256=20=D0=BE?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20=D1=85=D0=B0=D0=B1=D0=B0,=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D1=80=D0=B0=D1=81=D1=85=D0=BE=D0=B6=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B8=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D0=B8=20=D1=84=D0=B0=D0=B9=D0=BB=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D1=8B=D0=B2=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F,=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B5=20=D1=81=D0=B2=D0=B5=D1=80=D1=8F=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++ ...20\272\320\265\321\202\320\276\320\262.os" | 35 ++++++- ...20\236\321\202\320\262\320\265\321\202.os" | 2 + tests/checksum-test.os | 91 +++++++++++++++++++ ...20\260\320\245\320\260\320\261\320\260.os" | 4 +- 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 tests/checksum-test.os diff --git a/README.md b/README.md index 5a69f37..8d292f5 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,12 @@ $ opm install my-package Исходное описание ошибки уходит в отладочный журнал `oscript.app.opm`. При переборе нескольких серверов из `opm.cfg` недоступный сервер остаётся предупреждением, и поиск идёт дальше. +# Проверка контрольной суммы + +Хаб присылает sha256 файла пакета в заголовке `X-Checksum-Sha256`. opm сверяет с ним скачанный `.ospx` +и при расхождении отказывает в установке, не записывая файл. Ответ без заголовка (хаб без его поддержки, +файловый сервер) не сверяется. + # Сервера В настоящее время пакеты хранятся на двух серверах: - [hub.oscript.io](http://hub.oscript.io/download) - основной хаб пакетов diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 67134b2..b7295ab 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -44,9 +44,12 @@ Если Не Ответ = Неопределено Тогда Лог.Отладка("Файл получен"); - Ответ.ПолучитьТелоКакДвоичныеДанные().Записать(ПутьКФайлуПакета); + Данные = Ответ.ПолучитьТелоКакДвоичныеДанные(); + ОбъявленнаяСумма = ЗначениеЗаголовка(Ответ.Заголовки, "X-Checksum-Sha256"); Ответ.Закрыть(); Лог.Отладка("Соединение закрыто"); + ПроверитьКонтрольнуюСумму(Данные, ОбъявленнаяСумма, ИмяПакета); + Данные.Записать(ПутьКФайлуПакета); Иначе ТекстИсключения = СтрШаблон("Ошибка установки пакета %1: %2", ИмяПакета, ПричинаНеудачи(ФайлПакета, Полученное)); ВызватьИсключение ТекстИсключения; @@ -56,6 +59,36 @@ КонецФункции +// Хаб без поддержки контрольной суммы заголовка не присылает — такой файл не сверяем. +Процедура ПроверитьКонтрольнуюСумму(Знач Данные, Знач ОбъявленнаяСумма, Знач ИмяПакета) + + Если ПустаяСтрока(ОбъявленнаяСумма) Тогда + Возврат; + КонецЕсли; + + Хеширование = Новый ХешированиеДанных(ХешФункция.SHA256); + Хеширование.Добавить(Данные); + СуммаФайла = НРег(ПолучитьHexСтрокуИзДвоичныхДанных(Хеширование.ХешСумма)); + + Если СуммаФайла <> НРег(ОбъявленнаяСумма) Тогда + ВызватьИсключение СтрШаблон("Ошибка установки пакета %1: контрольная сумма файла %2 не совпала с объявленной хабом %3. " + + "Файл повреждён при скачивании или подменён", ИмяПакета, СуммаФайла, ОбъявленнаяСумма); + КонецЕсли; + +КонецПроцедуры + +Функция ЗначениеЗаголовка(Знач Заголовки, Знач Имя) + + Для Каждого Заголовок Из Заголовки Цикл + Если НРег(Заголовок.Ключ) = НРег(Имя) Тогда + Возврат СокрЛП(Заголовок.Значение); + КонецЕсли; + КонецЦикла; + + Возврат ""; + +КонецФункции + Функция ПричинаНеудачи(Знач ФайлПакета, Знач Полученное) КодСостояния = Полученное.КодСостояния; diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" index dc2a45c..a341e26 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" @@ -2,11 +2,13 @@ Перем Лог; Перем КодСостояния Экспорт; +Перем Заголовки Экспорт; Перем ИмяФайла; Процедура ПриСозданииОбъекта(Знач ПутьКФайлу, Знач ПодбиратьВерсию = Истина) ИмяФайла = ПутьКФайлу; + Заголовки = Новый Соответствие(); Файл = Новый Файл(ПутьКФайлу); Если Файл.Существует() Тогда diff --git a/tests/checksum-test.os b/tests/checksum-test.os new file mode 100644 index 0000000..1be6931 --- /dev/null +++ b/tests/checksum-test.os @@ -0,0 +1,91 @@ +#Использовать asserts +#Использовать tempfiles +#Использовать "../src/core" + +Перем юТест; +Перем Хаб; +Перем Ответ; +Перем ФайлПакета; + +// sha256 тела заглушки ответа «файл пакета» в UTF-8 +Перем СуммаТела; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ПринятьФайлССовпавшейСуммой"); + СписокТестов.Добавить("ТестДолжен_ОтказатьПриРасхожденииСуммы"); + СписокТестов.Добавить("ТестДолжен_ПропуститьСверкуБезЗаголовка"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + СуммаТела = "4d7a8fcdfff25ff76d6eb81129d1f6d8b655e927458293b9f14062c9dd5b6117"; + + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Хаб = ЗагрузитьЗаглушку("ЗаглушкаСервераПакетов.os"); + Хаб.ОбъявитьПакет("vrunner"); + Хаб.Отвечать(Ответ); + + НастройкиOpm.СброситьНастройки(); + НастройкиOpm.ДобавитьСерверПакетов(Хаб); + + ФайлПакета = ВременныеФайлы.НовоеИмяФайла("ospx"); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + НастройкиOpm.СброситьНастройки(); + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +Процедура ТестДолжен_ПринятьФайлССовпавшейСуммой() Экспорт + + Ответ.Заголовки.Вставить("x-checksum-sha256", ВРег(СуммаТела)); + + Скачать(); + + Ожидаем.Что(Новый Файл(ФайлПакета).Существует(), "файл записан").ЭтоИстина(); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьПриРасхожденииСуммы() Экспорт + + Ответ.Заголовки.Вставить("X-Checksum-Sha256", СтрСоединить(Новый Массив(65), "0")); + + Попытка + Скачать(); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "причина отказа").Содержит("контрольная сумма файла " + СуммаТела); + Ожидаем.Что(Новый Файл(ФайлПакета).Существует(), "файл не записан").ЭтоЛожь(); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ при расхождении контрольной суммы"); + +КонецПроцедуры + +Процедура ТестДолжен_ПропуститьСверкуБезЗаголовка() Экспорт + + Скачать(); + + Ожидаем.Что(Новый Файл(ФайлПакета).Существует(), "файл записан").ЭтоИстина(); + +КонецПроцедуры + +Процедура Скачать() + МенеджерПолучения = Новый МенеджерПолученияПакетов(); + МенеджерПолучения.ПолучитьПакет("vrunner", Неопределено, ФайлПакета); +КонецПроцедуры + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" index 27b3c72..a543df5 100644 --- "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" @@ -1,7 +1,8 @@ -// Ответ хаба с заданным кодом и текстом. +// Ответ хаба с заданным кодом, текстом и заголовками. Перем КодСостояния Экспорт; Перем Тело Экспорт; +Перем Заголовки Экспорт; Функция ПолучитьТелоКакДвоичныеДанные() Экспорт Возврат ПолучитьДвоичныеДанныеИзСтроки("файл пакета"); @@ -16,3 +17,4 @@ КодСостояния = 200; Тело = ""; +Заголовки = Новый Соответствие(); From f28e707c51a4dadb3c4f3b3df25645664eb3242d Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 07:08:06 +0400 Subject: [PATCH 08/17] =?UTF-8?q?=D0=90=D0=B4=D1=80=D0=B5=D1=81=20=D1=85?= =?UTF-8?q?=D0=B0=D0=B1=D0=B0=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD?= =?UTF-8?q?=D0=B4=D0=B5:=20opm=20install=20=D1=85=D0=BE=D1=81=D1=82[:?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=82]/[=D0=BF=D1=83=D0=BB/]=D0=BF=D0=B0?= =?UTF-8?q?=D0=BA=D0=B5=D1=82[@=D1=81=D0=BF=D0=B5=D1=86=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=86=D0=B8=D1=8F]=20=D1=81=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=20=D0=B8=20=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D1=81=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=20=D0=B8=D0=B7=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D0=B0=20=D0=B1=D0=B5=D0=B7=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B8=20=D0=B2=20opm.cfg,=20opm=20push=20--hub=20=D1=85=D0=BE?= =?UTF-8?q?=D1=81=D1=82[:=D0=BF=D0=BE=D1=80=D1=82][/=D0=BF=D1=83=D0=BB]=20?= =?UTF-8?q?=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D1=83=D0=B5=D1=82=20=D0=B2?= =?UTF-8?q?=20=D0=BF=D1=83=D0=BB;=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=B0=20=E2=80=94=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20=D0=90=D0=B4=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D0=B0=D0=A5=D0=B0=D0=B1=D0=B0,=20=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=20=D0=BF=D1=83=D0=BB=D0=B0=20=E2=80=94=20=D0=A1?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D1=8B=D0=9F=D0=B0=D0=BA=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=B2.=D0=98=D0=B7=D0=90=D0=B4=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D0=B0=20=D1=81=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B5=D0=B9=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=20=D0=B8=D0=B7=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=20=D1=82=D0=BE=D0=B3=D0=BE=20=D0=B6=D0=B5=20?= =?UTF-8?q?=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=B0;=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=81=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D1=81=D0=BE=D0=B1=D0=B8=D1=80=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20=D0=A1=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=9F=D0=B0=D0=BA?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=B2,=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=20?= =?UTF-8?q?=D1=85=D0=B0=D0=B1=D0=B0=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC;?= =?UTF-8?q?=20=D0=BE=D0=BF=D1=86=D0=B8=D1=8F=20push=20-m=20=D1=83=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D0=B5=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 25 +++ packagedef | 1 + ...20\260\320\275\320\264\320\260Opm_Push.os" | 138 +++++++++-------- ...266\320\265\320\275\320\270\321\217Opm.os" | 3 +- ...20\272\320\265\321\202\320\276\320\262.os" | 50 ++++++ ...20\260\320\245\320\260\320\261\320\260.os" | 110 ++++++++++++++ ...20\265\321\202\320\260\320\274\320\270.os" | 22 ++- ...20\272\320\265\321\202\320\276\320\262.os" | 34 +++++ ...20\272\320\265\321\202\320\276\320\262.os" | 4 + tests/hub-address-test.os | 142 ++++++++++++++++++ tests/push-target-test.os | 133 ++++++++++++++++ 11 files changed, 595 insertions(+), 67 deletions(-) create mode 100644 "src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" create mode 100644 tests/hub-address-test.os create mode 100644 tests/push-target-test.os diff --git a/README.md b/README.md index 8d292f5..fdeecf3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,31 @@ OneScript Package Manager Справка из википедии: [Система управления пакетами](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D0%BF%D0%B0%D0%BA%D0%B5%D1%82%D0%B0%D0%BC%D0%B8) +# Адрес хаба прямо в команде + +Установка принимает адрес пакета в хабе вместо имени, публикация — адрес пула опцией `--hub` +(или переменной окружения `OPM_HUB`); запись в `opm.cfg` для этого не нужна: + +```sh +opm install localhost:3380/corp/my-package@stable # хост, порт, пул, пакет, тег +opm install hub.example.com/asserts # пакет в пуле хаба по умолчанию +opm install https://hub.example.com/public/asserts # схема указана явно +opm push --hub localhost:3380/corp my-package-1.0.0.ospx +opm push --hub https://hub.example.com --token <токен> my-package-1.0.0.ospx +``` + +Формат адреса: `[схема://]хост[:порт][/пул][/пакет[@спецификация]]`. + +* На установке последний сегмент пути — пакет, сегмент перед ним — пул; без пула — пул хаба по умолчанию. + Зависимости пакета ставятся с того же сервера. +* Строка считается адресом, если в ней есть схема либо путь после хоста с портом, с точкой в имени + или `localhost`. Остальное — имя пакета. +* Без схемы берётся `http` для `localhost`, `127.*` и адреса с явным портом, иначе `https`. +* Авторизация берётся у сервера из `opm.cfg` с тем же адресом и портом. +* `opm push --hub` отправляет токен из опции `--token` (переменная окружения `GITHUB_OAUTH_TOKEN`) + или из настройки `ТокенПубликацииПакетов`. Канал при публикации в такой хаб необязателен. +* Опция `-m|--mirror` у `push` устарела и печатает предупреждение: вместо неё — `--hub`. + # Версия пакета после @ `install`, `update` и `download` принимают после `@` точную версию, имя тега или выражение версии: diff --git a/packagedef b/packagedef index f39977b..974e34e 100644 --- a/packagedef +++ b/packagedef @@ -47,6 +47,7 @@ .ОпределяетКласс("СерверПакетов", "src/core/Классы/СерверПакетов.os") .ОпределяетКласс("СериализацияМетаданныхПакета", "src/core/Классы/СериализацияМетаданныхПакета.os") .ОпределяетКласс("УстановкаПакета", "src/core/Классы/УстановкаПакета.os") + .ОпределяетМодуль("АдресаХаба", "src/core/Модули/АдресаХаба.os") .ОпределяетМодуль("КонстантыOpm", "src/core/Модули/КонстантыOpm.os") .ОпределяетМодуль("НастройкиOpm", "src/core/Модули/НастройкиOpm.os") .ОпределяетМодуль("РаботаСВерсиями", "src/core/Модули/РаботаСВерсиями.os") diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index 0603d2a..30a4d62 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -10,7 +10,11 @@ Процедура ОписаниеКоманды(Знач КомандаПриложения) Экспорт - КомандаПриложения.Опция("token", "", "Токен авторизации на GitHub.com + КомандаПриложения.Опция("hub", "", "Адрес хаба публикации: [схема://]хост[:порт][/пул]. + | Без пула - пул хаба по умолчанию.") + .ВОкружении("OPM_HUB"); + КомандаПриложения.Опция("token", "", "Токен авторизации в хабе пакетов: для OpenHub - персональный токен доступа, + | для хаба без OpenHub - токен GitHub.com | - Токен авторизации создается на странице https://github.com/settings/tokens | - Токен используется только для проверки авторизации на GitHub.com и прав на репозиторий, | выдача дополнительных разрешений в ""scopes"" НЕ требуется.") @@ -21,11 +25,11 @@ .ТПеречисление() .Перечисление("auto", "auto", "Автоматическое определение канала. | В случае отправки из ветки master гит-репозитория данный параметр можно опустить - будет использоваться канал ""stable"". - | В любых других случаях его заполнение обязательно.") + | В любых других случаях его заполнение обязательно, кроме публикации в хаб из опции <--hub>.") .Перечисление("stable", "stable", "Канал содержащий стабильные версии пакетов") .Перечисление("dev", "dev" , "Канал содержащий разработческие версии пакетов") .ВОкружении("OPM_HUB_CHANNEL"); - ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "СерверУдаленногоХранилища", "Имя сервера для публикации. + ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "", "УСТАРЕЛА, указывайте адрес хаба опцией <--hub>. Имя сервера для публикации. | Доступные сервера прописываются в конфигурационном файле opm.cfg, параметр 'СервераПакетов'.") .ВОкружении("OPM_HUB_MIRROR") .ТПеречисление(); @@ -46,6 +50,11 @@ МаскаФайлаПакетаСтарая = КомандаПриложения.ЗначениеОпции("file"); ИмяКаналаПубликации = КомандаПриложения.ЗначениеОпции("channel"); ИмяСервераПакетов = КомандаПриложения.ЗначениеОпции("mirror"); + АдресХаба = КомандаПриложения.ЗначениеОпции("hub"); + + Если ЗначениеЗаполнено(ИмяСервераПакетов) Тогда + Лог.Предупреждение("Опция <-m|--mirror> устарела и будет удалена. Используйте опцию <--hub>"); + КонецЕсли; Если ПустаяСтрока(ТокенАвторизации) Тогда НастройкиИзФайла = ПараметрыПриложенияOpm.СобратьНастройкиИзФайлов(); @@ -65,13 +74,14 @@ ФайлПакета = ПолучитьФайлПакета(МаскаФайлаПакета); - Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации); + Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации, ПустаяСтрока(АдресХаба)); - ОтправитьПакетВХаб(ТокенАвторизации, ФайлПакета, Канал, ИмяСервераПакетов); + ОтправитьПакетВХаб(ТокенАвторизации, ФайлПакета, Канал, СерверПубликации(АдресХаба, ИмяСервераПакетов)); КонецПроцедуры -Функция ПолучитьИмяКаналаПубликации(Знач ИмяКаналаПубликации) +// Хаб из опции --hub ведёт каналы тегами пакета: неопределённый канал уходит ему пустым. +Функция ПолучитьИмяКаналаПубликации(Знач ИмяКаналаПубликации, Знач КаналОбязателен) ГитРепозиторий = Новый ГитРепозиторий(); ГитРепозиторий.УстановитьРабочийКаталог(ТекущийКаталог()); @@ -80,18 +90,14 @@ Если КаналПубликации = "auto" Тогда - Если НЕ ГитРепозиторий.ЭтоРепозиторий() Тогда - ВызватьИсключение "Невозможно автоматически определить канал. " - + "Требуется явное указание в параметре --channel одного из значений: " + ДопустимыеИменаКаналовЗначения(); - КонецЕсли; - - ИмяВетки = ГитРепозиторий.ПолучитьТекущуюВетку(); - Если ИмяВетки <> "master" Тогда - ВызватьИсключение "Невозможно автоматически определить канал. " - + "Требуется явное указание в параметре --channel одного из значений: " + ДопустимыеИменаКаналовЗначения(); + Если ГитРепозиторий.ЭтоРепозиторий() И ГитРепозиторий.ПолучитьТекущуюВетку() = "master" Тогда + Возврат ДопустимыеИменаКаналов.Стабильный; + ИначеЕсли НЕ КаналОбязателен Тогда + Возврат ""; КонецЕсли; - Возврат ДопустимыеИменаКаналов.Стабильный; + ВызватьИсключение "Невозможно автоматически определить канал. " + + "Требуется явное указание в параметре --channel одного из значений: " + ДопустимыеИменаКаналовЗначения(); Иначе @@ -137,56 +143,46 @@ КонецФункции -Процедура ОтправитьПакетВХаб(Знач ТокенАвторизации, Знач ФайлПакета, Знач Канал, Знач ИмяСервераПакетов) - - ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ФайлПакета.ПолноеИмя); - ДвоичныеДанныеФайлаВBase64 = Base64Строка(ДвоичныеДанныеФайла); - - ДоступныеСервераПакетов = НастройкиOpm.ПолучитьНастройки().СервераПакетов; +// Сервер публикации: адрес хаба из опции --hub, сервер из opm.cfg по имени либо хаб по умолчанию. +// +// Параметры: +// АдресХаба - Строка - [схема://]хост[:порт][/пул]; пустой - публикация на сервер по имени. +// ИмяСервераПакетов - Строка - имя сервера из opm.cfg. +// +// Возвращаемое значение: +// СерверПакетов - сервер с ресурсом публикации. +// +Функция СерверПубликации(Знач АдресХаба, Знач ИмяСервераПакетов) Экспорт + + Если ЗначениеЗаполнено(АдресХаба) Тогда + Возврат СерверыПакетов.ИзАдреса(АдресаХаба.АдресПула(АдресХаба)); + КонецЕсли; - // Для настроек по умолчанию - Сервер = КонстантыOpm.СерверУдаленногоХранилища; - Ресурс = КонстантыOpm.РесурсПубликацииПакетов; - Авторизация = Неопределено; - ДополнительныеЗаголовки = Неопределено; - Таймаут = 600; - - Для Каждого НастройкаСервера Из ДоступныеСервераПакетов Цикл - + Для Каждого НастройкаСервера Из НастройкиOpm.ПолучитьНастройки().СервераПакетов Цикл Если СтрСравнить(НастройкаСервера.Имя, ИмяСервераПакетов) = 0 Тогда - Сервер = НастройкаСервера.Сервер; - Ресурс = НастройкаСервера.РесурсПубликацииПакетов; - Авторизация = НастройкаСервера.Авторизация; - ДополнительныеЗаголовки = НастройкаСервера.ДополнительныеЗаголовки; - Таймаут = НастройкаСервера.Таймаут; - Прервать; + Возврат НастройкаСервера; КонецЕсли; - КонецЦикла; - - Лог.Отладка("Сервер = %1", Сервер); - Лог.Отладка("Ресурс = %1", Ресурс); - Заголовки = Новый Соответствие(); - Если ЗначениеЗаполнено(Авторизация) Тогда - Заголовки.Вставить("Authorization", ОбщегоНазначенияOpm.ПрименитьПеременныеСреды(Авторизация)); - Иначе - Заголовки.Вставить("OAUTH-TOKEN", ТокенАвторизации); - КонецЕсли; - Заголовки.Вставить("FILE-NAME", ФайлПакета.Имя); - Заголовки.Вставить("CHANNEL", Канал); - Если ЗначениеЗаполнено(ДополнительныеЗаголовки) Тогда - Для Каждого мЗаголовок Из ДополнительныеЗаголовки Цикл - Заголовки.Вставить(мЗаголовок.Ключ, ОбщегоНазначенияOpm.ПрименитьПеременныеСреды(мЗаголовок.Значение)); - КонецЦикла; - КонецЕсли; - - Соединение = Новый HTTPСоединение(Сервер,,,,, Таймаут); - Запрос = Новый HTTPЗапрос(Ресурс, Заголовки); - Запрос.УстановитьТелоИзДвоичныхДанных(ДвоичныеДанныеФайла); - - Ответ = Соединение.ОтправитьДляОбработки(Запрос); - ТелоОтвета = Ответ.ПолучитьТелоКакСтроку(); + Возврат Новый СерверПакетов(КонстантыOpm.СерверУдаленногоХранилища, КонстантыOpm.СерверУдаленногоХранилища, , + КонстантыOpm.РесурсПубликацииПакетов, 0, 0, , , 600); + +КонецФункции + +// Отправляет файл пакета в хаб; отказ хаба - исключение с его сообщением. +// +// Параметры: +// ТокенАвторизации - Строка - токен публикации. +// ФайлПакета - Файл - файл .ospx. +// Канал - Строка - канал публикации. +// Сервер - СерверПакетов - сервер публикации. +// +Процедура ОтправитьПакетВХаб(Знач ТокенАвторизации, Знач ФайлПакета, Знач Канал, Знач Сервер) Экспорт + + Лог.Отладка("Публикация на сервер %1", Сервер.ПолучитьИмя()); + + Ответ = Сервер.ОтправитьПакет(ФайлПакета, Канал, ТокенАвторизации); + ТелоОтвета = СообщениеХаба(Ответ.ПолучитьТелоКакСтроку()); Если Не КодУспешнойОтправки(Ответ.КодСостояния) Тогда ВызватьИсключение ТелоОтвета; @@ -196,6 +192,26 @@ КонецПроцедуры +// Хаб OpenHub отвечает JSON {"КодСостояние", "Сообщение"}: пользователю - только сообщение. +Функция СообщениеХаба(Знач ТелоОтвета) + + Попытка + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(ТелоОтвета); + Ответ = ПрочитатьJSON(Чтение, Истина); + Чтение.Закрыть(); + Исключение + Возврат ТелоОтвета; + КонецПопытки; + + Если ТипЗнч(Ответ) = Тип("Соответствие") И Ответ["Сообщение"] <> Неопределено Тогда + Возврат Ответ["Сообщение"]; + КонецЕсли; + + Возврат ТелоОтвета; + +КонецФункции + Функция КодУспешнойОтправки(Знач КодСостояния) Возврат КодСостояния = 200 // OK diff --git "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" index 6e04508..fcee45d 100644 --- "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" +++ "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" @@ -22,7 +22,8 @@ // Функция ОписаниеАргументаПакет() Экспорт Возврат "Имя пакета в хабе. После @ можно указать точную версию (пакет@1.0.0), - | имя тега (пакет@stable) или выражение версии (пакет@^1.4)."; + | имя тега (пакет@stable) или выражение версии (пакет@^1.4). + | Вместо имени можно указать адрес пакета в хабе: [схема://]хост[:порт]/[пул/]пакет[@спецификация]."; КонецФункции Процедура НастроитьOpm() Экспорт diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 92612ef..f3609c9 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -123,6 +123,56 @@ КонецФункции +// Отправляет файл пакета на ресурс публикации. +// +// Параметры: +// ФайлПакета - Файл - файл .ospx. +// Канал - Строка - значение заголовка CHANNEL. +// Токен - Строка - токен публикации; пустой - без токена. +// +// Возвращаемое значение: +// HTTPОтвет - ответ сервера. +// +Функция ОтправитьПакет(Знач ФайлПакета, Знач Канал, Знач Токен) Экспорт + + Соединение = ИнициализироватьСоединение(); + Запрос = ЗапросПубликации(ФайлПакета, Канал, Токен); + + Попытка + Возврат Соединение.ОтправитьДляОбработки(Запрос); + Исключение + Лог.Отладка("Публикация в %1%2 не удалась: %3", Сервер, РесурсПубликацииПакетов, ОписаниеОшибки()); + ВызватьИсключение СтрШаблон("Не удалось подключиться к %1", АдресДляСообщений()); + КонецПопытки; + +КонецФункции + +// Запрос публикации файла пакета. Токен уходит заголовком OAUTH-TOKEN, +// если у сервера нет своей авторизации. +// +// Параметры: +// ФайлПакета - Файл - файл .ospx. +// Канал - Строка - значение заголовка CHANNEL. +// Токен - Строка - токен публикации; пустой - без токена. +// +// Возвращаемое значение: +// HTTPЗапрос - запрос с заголовками и телом. +// +Функция ЗапросПубликации(Знач ФайлПакета, Знач Канал, Знач Токен) Экспорт + + Запрос = Новый HTTPЗапрос(РесурсПубликацииПакетов); + Запрос.Заголовки.Вставить("FILE-NAME", ФайлПакета.Имя); + Запрос.Заголовки.Вставить("CHANNEL", Канал); + Если ЗначениеЗаполнено(Токен) И Не ЗначениеЗаполнено(Авторизация) Тогда + Запрос.Заголовки.Вставить("OAUTH-TOKEN", Токен); + КонецЕсли; + ДобавитьЗаголовки(Запрос); + Запрос.УстановитьТелоИзДвоичныхДанных(Новый ДвоичныеДанные(ФайлПакета.ПолноеИмя)); + + Возврат Запрос; + +КонецФункции + // Платформа оставляет плюс в адресе как есть, а в строке запроса он читается пробелом. Функция ЧастьАдреса(Знач Значение) Возврат КодироватьСтроку(Значение, СпособКодированияСтроки.КодировкаURL); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" new file mode 100644 index 0000000..a111caf --- /dev/null +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" @@ -0,0 +1,110 @@ +// Адрес хаба в командной строке: [схема://]хост[:порт][/пул][/пакет[@спецификация]]. + +Перем ФорматАдреса; + +// Похожа ли строка на адрес хаба, а не на имя пакета: есть схема либо путь после хоста, +// у которого есть порт, точка в имени или это localhost. +// +// Параметры: +// Значение - Строка - аргумент команды. +// +// Возвращаемое значение: +// Булево - Истина, если строка - адрес хаба. +// +Функция ЭтоАдресХаба(Знач Значение) Экспорт + + Части = ЧастиАдреса(Значение); + Если Части = Неопределено Тогда + Возврат Ложь; + КонецЕсли; + + Возврат ЗначениеЗаполнено(Части.Схема) + Или ЗначениеЗаполнено(Части.Путь) + И (ЗначениеЗаполнено(Части.Порт) Или Части.Хост = "localhost" Или СтрНайти(Части.Хост, ".") > 1); + +КонецФункции + +// Разбирает адрес пакета: последний сегмент пути - пакет, сегмент перед ним - пул. +// +// Параметры: +// Адрес - Строка - [схема://]хост[:порт]/[пул/]пакет[@спецификация]. +// +// Возвращаемое значение: +// Структура - Сервер (Строка - схема и хост), Порт (Число, 0 - порт схемы), +// Пул (Строка, пустая - пул хаба по умолчанию), Пакет (Строка - имя и спецификация после @). +// +Функция АдресПакета(Знач Адрес) Экспорт + + Результат = Разобрать(Адрес, "[схема://]хост[:порт]/[пул/]пакет[@спецификация]", 1, 2); + Результат.Вставить("Пакет", Результат.Сегменты[Результат.Сегменты.ВГраница()]); + Если Результат.Сегменты.Количество() = 2 Тогда + Результат.Пул = Результат.Сегменты[0]; + КонецЕсли; + + Возврат Результат; + +КонецФункции + +// Разбирает адрес пула, в который публикуют пакеты. +// +// Параметры: +// Адрес - Строка - [схема://]хост[:порт][/пул]. +// +// Возвращаемое значение: +// Структура - Сервер (Строка - схема и хост), Порт (Число, 0 - порт схемы), +// Пул (Строка, пустая - пул хаба по умолчанию). +// +Функция АдресПула(Знач Адрес) Экспорт + + Результат = Разобрать(Адрес, "[схема://]хост[:порт][/пул]", 0, 1); + Если Результат.Сегменты.Количество() = 1 Тогда + Результат.Пул = Результат.Сегменты[0]; + КонецЕсли; + + Возврат Результат; + +КонецФункции + +Функция Разобрать(Знач Адрес, Знач Форма, Знач НаименьшееЧислоСегментов, Знач НаибольшееЧислоСегментов) + + Части = ЧастиАдреса(Адрес); + Если Части <> Неопределено Тогда + Сегменты = СтрРазделить(Части.Путь, "/", Ложь); + КонецЕсли; + + Если Части = Неопределено + Или Сегменты.Количество() < НаименьшееЧислоСегментов + Или Сегменты.Количество() > НаибольшееЧислоСегментов Тогда + ВызватьИсключение СтрШаблон("Адрес <%1> не разобран: ожидалось %2", Адрес, Форма); + КонецЕсли; + + Схема = Части.Схема; + Если ПустаяСтрока(Схема) Тогда + // без схемы открытый канал - только для своей машины и адреса с явным портом + ЭтотКомпьютер = Части.Хост = "localhost" Или СтрНачинаетсяС(Части.Хост, "127."); + Схема = ?(ЭтотКомпьютер Или ЗначениеЗаполнено(Части.Порт), "http", "https"); + КонецЕсли; + + Возврат Новый Структура("Сервер, Порт, Пул, Сегменты", + НРег(Схема) + "://" + Части.Хост, + ?(ЗначениеЗаполнено(Части.Порт), Число(Части.Порт), 0), + "", + Сегменты); + +КонецФункции + +Функция ЧастиАдреса(Знач Адрес) + + Совпадения = ФорматАдреса.НайтиСовпадения(СокрЛП(Адрес)); + Если Совпадения.Количество() = 0 Тогда + Возврат Неопределено; + КонецЕсли; + + Группы = Совпадения[0].Группы; + Возврат Новый Структура("Схема, Хост, Порт, Путь", + Группы[1].Значение, НРег(Группы[2].Значение), Группы[3].Значение, Группы[4].Значение); + +КонецФункции + +ФорматАдреса = Новый РегулярноеВыражение("^(?:(https?)://)?([^/:]+)(?::(\d+))?(/.*)?$"); +ФорматАдреса.ИгнорироватьРегистр = Истина; diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" index eda4708..ab5d775 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" @@ -90,17 +90,29 @@ Процедура УстановитьПакетИзОблака(Знач ИмяПакета, Знач РежимУстановки, Знач ЦелевойКаталог, Знач НастройкаУстановки = Неопределено) Экспорт Лог.Отладка("Устанавливаю пакет <%1> из доступного хаба", ИмяПакета); + + Если НастройкаУстановки = Неопределено Тогда + НастройкаУстановки = ПолучитьНастройкуУстановки(); + КонецЕсли; + ИмяСервера = НастройкаУстановки.ИмяСервера; + + // пакет и его зависимости ставятся с сервера из адреса + Если АдресаХаба.ЭтоАдресХаба(ИмяПакета) Тогда + Адрес = АдресаХаба.АдресПакета(ИмяПакета); + Сервер = СерверыПакетов.ИзАдреса(Адрес); + НастройкиOpm.ДобавитьСерверПакетов(Сервер); + ВнутреннийМенеджерПолученияПакетов = Неопределено; + ИмяСервера = Сервер.ПолучитьИмя(); + ИмяПакета = Адрес.Пакет; + КонецЕсли; + ИмяВерсияПакета = РаботаСВерсиями.РазобратьИмяПакета(ИмяПакета); ЭтоЗависимость = Ложь; Если РежимУстановки = РежимУстановкиПакетов.Локально Тогда ЭтоЗависимость = Истина; КонеЦесли; - Если НастройкаУстановки = Неопределено Тогда - НастройкаУстановки = ПолучитьНастройкуУстановки(); - КонецЕсли; - - МенеджерУстановки = Новый МенеджерУстановкиПакетов(РежимУстановки, ЦелевойКаталог, , НастройкаУстановки.ИмяСервера); + МенеджерУстановки = Новый МенеджерУстановкиПакетов(РежимУстановки, ЦелевойКаталог, , ИмяСервера); МенеджерУстановки.УстанавливатьЗависимости(НастройкаУстановки.УстанавливатьЗависимости); МенеджерУстановки.СоздаватьФайлыЗапуска(НастройкаУстановки.СоздаватьФайлыЗапуска); МенеджерУстановки.УстанавливатьЗависимостиРазработчика(НастройкаУстановки.УстанавливатьЗависимостиРазработчика); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index d44abf1..d66e612 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -27,6 +27,40 @@ КонецФункции +// Сервер пакетов по адресу хаба из командной строки. Авторизация берётся у сервера +// из настроек с тем же адресом и портом. +// +// Параметры: +// Адрес - Структура - результат АдресаХаба.АдресПакета или АдресаХаба.АдресПула. +// +// Возвращаемое значение: +// СерверПакетов - сервер выдачи и публикации пула. +// +Функция ИзАдреса(Знач Адрес) Экспорт + + Имя = Адрес.Сервер + ?(Адрес.Порт = 0, "", ":" + Формат(Адрес.Порт, "ЧГ=0")); + ПутьВыдачи = "/download/"; + ПутьПубликации = "/push"; + Если ЗначениеЗаполнено(Адрес.Пул) Тогда + Имя = Имя + "/" + Адрес.Пул; + ПутьВыдачи = СтрШаблон("/api/v1/pools/%1/download/", Адрес.Пул); + ПутьПубликации = СтрШаблон("/api/v1/pools/%1/push", Адрес.Пул); + КонецЕсли; + + Авторизация = ""; + Для Каждого Настройка Из НастройкиOpm.ПолучитьНастройки().СервераПакетов Цикл + Если ТипЗнч(Настройка) = Тип("СерверПакетов") И ЗначениеЗаполнено(Настройка.Авторизация) + И НРег(Настройка.Сервер) = Адрес.Сервер И Настройка.Порт = Адрес.Порт Тогда + Авторизация = Настройка.Авторизация; + Прервать; + КонецЕсли; + КонецЦикла; + + Возврат Новый СерверПакетов(Имя, Адрес.Сервер, ПутьВыдачи, ПутьПубликации, Адрес.Порт, 0, + Авторизация, Новый Соответствие, 600); + +КонецФункции + Функция ОсновнойСервер() Экспорт Возврат Новый СерверПакетов("ОсновнойСерверПакетов", КонстантыOpm.СерверУдаленногоХранилища, diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 3a13529..0c4486e 100644 --- "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -40,3 +40,7 @@ Запросы.Добавить(Новый Структура("ИмяРесурса, Спецификация", ИмяРесурса, Спецификация)); Возврат Ответ; КонецФункции + +Функция ОтправитьПакет(Знач ФайлПакета, Знач Канал, Знач Токен) Экспорт + Возврат Ответ; +КонецФункции diff --git a/tests/hub-address-test.os b/tests/hub-address-test.os new file mode 100644 index 0000000..ca3c4a3 --- /dev/null +++ b/tests/hub-address-test.os @@ -0,0 +1,142 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ОтличитьАдресОтИмениПакета"); + СписокТестов.Добавить("ТестДолжен_РазобратьАдресПакетаСПуломИСпецификацией"); + СписокТестов.Добавить("ТестДолжен_РазобратьАдресПакетаВПулеПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ВыбратьСхемуПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаНеразобранномАдресе"); + СписокТестов.Добавить("ТестДолжен_СобратьСерверПулаПоАдресу"); + СписокТестов.Добавить("ТестДолжен_ВзятьАвторизациюАдресаИзНастроек"); + СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетССервераИзАдреса"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + НастройкиOpm.СброситьНастройки(); +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + НастройкиOpm.СброситьНастройки(); +КонецПроцедуры + +Процедура ТестДолжен_ОтличитьАдресОтИмениПакета() Экспорт + + Для Каждого Адрес Из СтрРазделить("hub.example/pkg|https://hub|http://hub/corp/pkg|localhost/pkg|hub:3380/pkg", "|") Цикл + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба(Адрес), Адрес).ЭтоИстина(); + КонецЦикла; + + Для Каждого Имя Из СтрРазделить("pkg|pkg@1.0.0|pkg@^1.4|hub.example|dir/pkg|./pkg|../dir/pkg|C:\dir\pkg", "|") Цикл + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба(Имя), Имя).ЭтоЛожь(); + КонецЦикла; + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьАдресПакетаСПуломИСпецификацией() Экспорт + + Адрес = АдресаХаба.АдресПакета("https://Hub.Example:8443/corp/pkg@stable"); + + Ожидаем.Что(Адрес.Сервер).Равно("https://hub.example"); + Ожидаем.Что(Адрес.Порт).Равно(8443); + Ожидаем.Что(Адрес.Пул).Равно("corp"); + Ожидаем.Что(Адрес.Пакет).Равно("pkg@stable"); + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьАдресПакетаВПулеПоУмолчанию() Экспорт + + Адрес = АдресаХаба.АдресПакета("hub.example/pkg/"); + + Ожидаем.Что(Адрес.Пул).Равно(""); + Ожидаем.Что(Адрес.Пакет).Равно("pkg"); + Ожидаем.Что(Адрес.Порт).Равно(0); + +КонецПроцедуры + +Процедура ТестДолжен_ВыбратьСхемуПоУмолчанию() Экспорт + + Ожидаем.Что(АдресаХаба.АдресПакета("hub.example/pkg").Сервер, "публичное имя").Равно("https://hub.example"); + Ожидаем.Что(АдресаХаба.АдресПакета("localhost/pkg").Сервер, "localhost").Равно("http://localhost"); + Ожидаем.Что(АдресаХаба.АдресПакета("127.0.0.1/pkg").Сервер, "127.0.0.1").Равно("http://127.0.0.1"); + Ожидаем.Что(АдресаХаба.АдресПакета("hub.example:3380/pkg").Сервер, "явный порт").Равно("http://hub.example"); + Ожидаем.Что(АдресаХаба.АдресПула("http://hub.example").Сервер, "явная схема").Равно("http://hub.example"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаНеразобранномАдресе() Экспорт + + Для Каждого Адрес Из СтрРазделить("hub.example|hub.example/a/b/c|hub.example:порт/pkg|ftp://hub/pkg", "|") Цикл + Ожидаем.Что(АдресаХаба).Метод("АдресПакета", ЗначенияПараметров(Адрес)) + .ВыбрасываетИсключение("ожидалось [схема://]хост[:порт]/[пул/]пакет[@спецификация]"); + КонецЦикла; + + Ожидаем.Что(АдресаХаба).Метод("АдресПула", ЗначенияПараметров("hub.example/corp/pkg")) + .ВыбрасываетИсключение("ожидалось [схема://]хост[:порт][/пул]"); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьСерверПулаПоАдресу() Экспорт + + Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.АдресПула("hub.example:3380/corp")); + + Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("http://hub.example:3380/corp"); + Ожидаем.Что(Сервер.Сервер).Равно("http://hub.example"); + Ожидаем.Что(Сервер.Порт).Равно(3380); + Ожидаем.Что(Сервер.ПутьНаСервере).Равно("/api/v1/pools/corp/download/"); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); + + Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.АдресПула("hub.example")); + + Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("https://hub.example"); + Ожидаем.Что(Сервер.ПутьНаСервере, "пул по умолчанию").Равно("/download/"); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов, "пул по умолчанию").Равно("/push"); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьАвторизациюАдресаИзНастроек() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("чужой", "https://other.example", , , 0, 0, "Bearer чужой")); + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("свой", "https://HUB.example", , , 0, 0, "Bearer свой")); + + Ожидаем.Что(СерверыПакетов.ИзАдреса(АдресаХаба.АдресПакета("hub.example/corp/pkg")).Авторизация) + .Равно("Bearer свой"); + Ожидаем.Что(СерверыПакетов.ИзАдреса(АдресаХаба.АдресПакета("hub.example:8443/corp/pkg")).Авторизация, "другой порт") + .Равно(""); + +КонецПроцедуры + +// Порт 1 на своей машине не слушают, а заглушка не даёт подставить серверы по умолчанию: сеть не нужна. +Процедура ТестДолжен_УстанавливатьПакетССервераИзАдреса() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, + "fixtures", "ЗаглушкаСервераПакетов.os"))); + + Попытка + РаботаСПакетами.УстановитьПакетИзОблака("127.0.0.1:1/corp/pkg@1.0.0", РежимУстановкиПакетов.Локально, + ОбъединитьПути(юТест.ИмяВременногоФайла(), "oscript_modules")); + ВызватьИсключение "установка без хаба должна отказать"; + Исключение + Ожидаем.Что(ОписаниеОшибки()).Содержит("Ошибка установки пакета pkg"); + КонецПопытки; + + Сервер = НастройкиOpm.ПолучитьНастройки().СервераПакетов[1]; + Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("http://127.0.0.1:1/corp"); + Ожидаем.Что(Сервер.ПутьНаСервере).Равно("/api/v1/pools/corp/download/"); + +КонецПроцедуры + +Функция ЗначенияПараметров(Знач Значение) + Параметры = Новый Массив; + Параметры.Добавить(Значение); + Возврат Параметры; +КонецФункции diff --git a/tests/push-target-test.os b/tests/push-target-test.os new file mode 100644 index 0000000..26f46e5 --- /dev/null +++ b/tests/push-target-test.os @@ -0,0 +1,133 @@ +#Использовать asserts +#Использовать tempfiles +#Использовать "../src/core" +#Использовать "../src/cmd" + +Перем юТест; +Перем Пуш; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ПубликоватьВПулИзОпцииХаба"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаАдресеПакетаВОпцииХаба"); + СписокТестов.Добавить("ТестДолжен_ПубликоватьНаСерверИзНастроекПоИмени"); + СписокТестов.Добавить("ТестДолжен_ПубликоватьВХабПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ОтправитьТокенИФайлВЗапросеПубликации"); + СписокТестов.Добавить("ТестДолжен_НеОтправлятьТокенСерверуСоСвоейАвторизацией"); + СписокТестов.Добавить("ТестДолжен_ПоказатьСообщениеХабаПриОтказе"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + Пуш = Новый КомандаOpm_Push(); + НастройкиOpm.СброситьНастройки(); +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + НастройкиOpm.СброситьНастройки(); + ВременныеФайлы.Удалить(); +КонецПроцедуры + +Процедура ТестДолжен_ПубликоватьВПулИзОпцииХаба() Экспорт + + Сервер = Пуш.СерверПубликации("127.0.0.1:3380/corp", ""); + + Ожидаем.Что(Сервер.Сервер).Равно("http://127.0.0.1"); + Ожидаем.Что(Сервер.Порт).Равно(3380); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаАдресеПакетаВОпцииХаба() Экспорт + + Параметры = Новый Массив; + Параметры.Добавить("hub.example/corp/pkg"); + Параметры.Добавить(""); + + Ожидаем.Что(Пуш).Метод("СерверПубликации", Параметры).ВыбрасываетИсключение("ожидалось [схема://]хост[:порт][/пул]"); + +КонецПроцедуры + +Процедура ТестДолжен_ПубликоватьНаСерверИзНастроекПоИмени() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("corp", "https://hub.example", , "/api/v1/pools/corp/push")); + + Ожидаем.Что(Пуш.СерверПубликации("", "corp").РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); + +КонецПроцедуры + +Процедура ТестДолжен_ПубликоватьВХабПоУмолчанию() Экспорт + + Сервер = Пуш.СерверПубликации("", ""); + + Ожидаем.Что(Сервер.Сервер).Равно(КонстантыOpm.СерверУдаленногоХранилища); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно(КонстантыOpm.РесурсПубликацииПакетов); + +КонецПроцедуры + +Процедура ТестДолжен_ОтправитьТокенИФайлВЗапросеПубликации() Экспорт + + Сервер = Пуш.СерверПубликации("hub.example/corp", ""); + ФайлПакета = ФайлПакетаПробы(); + + Запрос = Сервер.ЗапросПубликации(ФайлПакета, "stable", "токен"); + + Ожидаем.Что(Запрос.АдресРесурса).Равно("/api/v1/pools/corp/push"); + Ожидаем.Что(Запрос.Заголовки["FILE-NAME"]).Равно(ФайлПакета.Имя); + Ожидаем.Что(Запрос.Заголовки["CHANNEL"]).Равно("stable"); + Ожидаем.Что(Запрос.Заголовки["OAUTH-TOKEN"]).Равно("токен"); + +КонецПроцедуры + +Процедура ТестДолжен_НеОтправлятьТокенСерверуСоСвоейАвторизацией() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("corp", "https://hub.example", , , 0, 0, "Bearer из настроек")); + + Запрос = Пуш.СерверПубликации("hub.example/corp", "").ЗапросПубликации(ФайлПакетаПробы(), "", "токен"); + + Ожидаем.Что(Запрос.Заголовки["Authorization"]).Равно("Bearer из настроек"); + Ожидаем.Что(Запрос.Заголовки["OAUTH-TOKEN"]).Равно(Неопределено); + +КонецПроцедуры + +Процедура ТестДолжен_ПоказатьСообщениеХабаПриОтказе() Экспорт + + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Ответ.КодСостояния = 409; + Ответ.Тело = "{""КодСостояние"": 409, ""Сообщение"": ""Версия 1.0.0 уже опубликована""}"; + Сервер = ЗагрузитьЗаглушку("ЗаглушкаСервераПакетов.os"); + Сервер.Отвечать(Ответ); + + Параметры = Новый Массив; + Параметры.Добавить("токен"); + Параметры.Добавить(ФайлПакетаПробы()); + Параметры.Добавить(""); + Параметры.Добавить(Сервер); + + Ожидаем.Что(Пуш).Метод("ОтправитьПакетВХаб", Параметры).ВыбрасываетИсключение("Версия 1.0.0 уже опубликована"); + + Ответ.Тело = "Bad Gateway"; + Ожидаем.Что(Пуш).Метод("ОтправитьПакетВХаб", Параметры).ВыбрасываетИсключение("Bad Gateway"); + +КонецПроцедуры + +Функция ФайлПакетаПробы() + + ПутьКФайлу = ВременныеФайлы.НовоеИмяФайла("ospx"); + Запись = Новый ЗаписьТекста(ПутьКФайлу); + Запись.Записать("содержимое пакета"); + Запись.Закрыть(); + + Возврат Новый Файл(ПутьКФайлу); + +КонецФункции + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции From d0aa2a5afc712b8fc71c523f1701787cda53f361 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 07:11:22 +0400 Subject: [PATCH 09/17] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B7=D0=BD=D0=B0?= =?UTF-8?q?=D0=BA=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=B0=20=D1=85=D0=B0?= =?UTF-8?q?=D0=B1=D0=B0:=20=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=85=D0=BE=D1=81=D1=82=D0=B0=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=83=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\265\321\201\320\260\320\245\320\260\320\261\320\260.os" | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" index a111caf..42b8511 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" @@ -18,9 +18,8 @@ Возврат Ложь; КонецЕсли; - Возврат ЗначениеЗаполнено(Части.Схема) - Или ЗначениеЗаполнено(Части.Путь) - И (ЗначениеЗаполнено(Части.Порт) Или Части.Хост = "localhost" Или СтрНайти(Части.Хост, ".") > 1); + ХостЯвный = ЗначениеЗаполнено(Части.Порт) Или Части.Хост = "localhost" Или СтрНайти(Части.Хост, ".") > 1; + Возврат ЗначениеЗаполнено(Части.Схема) Или (ЗначениеЗаполнено(Части.Путь) И ХостЯвный); КонецФункции From d59fd68282f594880dda44bb3a54fc7f6702e487 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 08:22:52 +0400 Subject: [PATCH 10/17] =?UTF-8?q?=D0=A5=D0=B0=D0=B1=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=83=D0=BB=20=D0=BE=D0=BF=D1=86=D0=B8=D1=8F=D0=BC=D0=B8:=20--?= =?UTF-8?q?hub=20=D0=BF=D1=80=D0=B8=D0=BD=D0=B8=D0=BC=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20[=D1=81=D1=85=D0=B5?= =?UTF-8?q?=D0=BC=D0=B0://]=D1=85=D0=BE=D1=81=D1=82[:=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=82],=20=D0=BF=D1=83=D1=82=D1=8C=20=D0=B2=20=D0=BD=D1=91?= =?UTF-8?q?=D0=BC=20=E2=80=94=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=20=D1=81=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=81=D0=BA=D0=B0=D0=B7=D0=BA=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=20--pool;=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F?= =?UTF-8?q?=20=D0=BE=D0=BF=D1=86=D0=B8=D1=8F=20--pool=20(OPM=5FPOOL)=20?= =?UTF-8?q?=D1=83=20push=20=D0=B8=20install,=20--pool=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=20--hub=20=E2=80=94=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7;=20instal?= =?UTF-8?q?l=20--hub=20[--pool]=20=D1=81=D1=82=D0=B0=D0=B2=D0=B8=D1=82=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D1=8B=20=D0=B8=D0=B7=20=D0=B0?= =?UTF-8?q?=D1=80=D0=B3=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=B8?= =?UTF-8?q?=20=D0=B8=D1=85=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20=D1=81=20=D0=BF=D1=83=D0=BB=D0=B0=20?= =?UTF-8?q?=D1=85=D0=B0=D0=B1=D0=B0=20=D1=82=D0=B5=D0=BC=20=D0=B6=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=83=D1=82=D1=91=D0=BC,=20=D1=87=D1=82=D0=BE=20=D0=B8?= =?UTF-8?q?=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=20=D0=B2=20=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20(=D0=A0?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=A1=D0=9F=D0=B0=D0=BA=D0=B5?= =?UTF-8?q?=D1=82=D0=B0=D0=BC=D0=B8.=D0=9F=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=D0=A1=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=A5=D0=B0=D0=B1=D0=B0),=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B5=20=D1=81=20--hub=20=D0=B8=20--hub=20=D1=81=20-a,?= =?UTF-8?q?=20-f,=20-u,=20-m=20=E2=80=94=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7;?= =?UTF-8?q?=20=D0=90=D0=B4=D1=80=D0=B5=D1=81=D0=B0=D0=A5=D0=B0=D0=B1=D0=B0?= =?UTF-8?q?.=D0=90=D0=B4=D1=80=D0=B5=D1=81=D0=9F=D1=83=D0=BB=D0=B0=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D1=91=D0=BD=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=90=D0=B4=D1=80=D0=B5=D1=81(=D0=A5=D0=B0=D0=B1,=20=D0=9F?= =?UTF-8?q?=D1=83=D0=BB);=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B0=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D0=B0,=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=B8=20=D1=81=20=D1=85=D0=B0?= =?UTF-8?q?=D0=B1=D0=B0=20=D0=B8=D0=B7=20=D0=BE=D0=BF=D1=86=D0=B8=D0=B9;?= =?UTF-8?q?=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +++--- ...260\320\275\320\264\320\260Opm_Install.os" | 38 ++++++++++++++ ...20\260\320\275\320\264\320\260Opm_Push.os" | 19 ++++--- ...20\260\320\245\320\260\320\261\320\260.os" | 17 ++++--- ...20\265\321\202\320\260\320\274\320\270.os" | 23 +++++++-- ...20\272\320\265\321\202\320\276\320\262.os" | 2 +- tests/hub-address-test.os | 49 +++++++++++++++++-- tests/push-target-test.os | 26 +++++++--- 8 files changed, 150 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index fdeecf3..89e582d 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,23 @@ OneScript Package Manager # Адрес хаба прямо в команде -Установка принимает адрес пакета в хабе вместо имени, публикация — адрес пула опцией `--hub` -(или переменной окружения `OPM_HUB`); запись в `opm.cfg` для этого не нужна: +Установка принимает адрес пакета в хабе вместо имени либо хаб опцией `--hub` и пул опцией `--pool` +(переменные окружения `OPM_HUB` и `OPM_POOL`), публикация — только опциями; запись в `opm.cfg` не нужна: ```sh opm install localhost:3380/corp/my-package@stable # хост, порт, пул, пакет, тег opm install hub.example.com/asserts # пакет в пуле хаба по умолчанию opm install https://hub.example.com/public/asserts # схема указана явно -opm push --hub localhost:3380/corp my-package-1.0.0.ospx +opm install --hub localhost:3380 --pool corp my-package@stable +opm push --hub localhost:3380 --pool corp my-package-1.0.0.ospx opm push --hub https://hub.example.com --token <токен> my-package-1.0.0.ospx ``` -Формат адреса: `[схема://]хост[:порт][/пул][/пакет[@спецификация]]`. +Адрес пакета: `[схема://]хост[:порт]/[пул/]пакет[@спецификация]`; в `--hub` — `[схема://]хост[:порт]` без пути. -* На установке последний сегмент пути — пакет, сегмент перед ним — пул; без пула — пул хаба по умолчанию. - Зависимости пакета ставятся с того же сервера. +* В адресе пакета последний сегмент пути — пакет, сегмент перед ним — пул; без пула — пул хаба по умолчанию. + Зависимости пакета ставятся с того же сервера, у `install --hub` — с хаба из опций. +* Отказ: путь в `--hub`, `--pool` без `--hub`, адрес пакета вместе с `--hub`, `install --hub` вместе с `-a`, `-f`, `-u`, `-m`. * Строка считается адресом, если в ней есть схема либо путь после хоста с портом, с точкой в имени или `localhost`. Остальное — имя пакета. * Без схемы берётся `http` для `localhost`, `127.*` и адреса с явным портом, иначе `https`. diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" index 2d6e5c4..9a55dea 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" @@ -23,6 +23,11 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; + КомандаПриложения.Опция("hub", "", "Адрес хаба, с которого ставятся пакеты и их зависимости: [схема://]хост[:порт].") + .ВОкружении("OPM_HUB"); + КомандаПриложения.Опция("pool", "", "Пул хаба из опции <--hub>, без пула - пул хаба по умолчанию.") + .ВОкружении("OPM_POOL"); + КомандаПриложения.Аргумент("PACKAGE", "", ПараметрыПриложенияOpm.ОписаниеАргументаПакет()) .ТМассивСтрок() .Обязательный(Ложь); @@ -44,6 +49,8 @@ НеобходимоУстановитьЗависимости = Не КомандаПриложения.ЗначениеОпции("skip-install-deps"); СоздаватьФайлыЗапуска = НЕ КомандаПриложения.ЗначениеОпции("skip-create-app"); ИмяСервера = КомандаПриложения.ЗначениеОпции("mirror"); + Хаб = КомандаПриложения.ЗначениеОпции("hub"); + Пул = КомандаПриложения.ЗначениеОпции("pool"); РежимУстановки = РежимУстановкиПакетов.Глобально; Если УстановкаВЛокальныйКаталог Тогда @@ -78,6 +85,14 @@ НастройкаУстановки.СоздаватьФайлыЗапуска = СоздаватьФайлыЗапуска; НастройкаУстановки.ИмяСервера = ИмяСервера; + Если ЗначениеЗаполнено(Хаб) Или ЗначениеЗаполнено(Пул) Тогда + Если УстановкаВсехПакетов Или ЗначениеЗаполнено(ФайлПакетаУстановки) Или ЗначениеЗаполнено(ИнтернетСсылкаНаПакет) + Или ЗначениеЗаполнено(ИмяСервера) Тогда + ВызватьИсключение "Опция <--hub> не сочетается с опциями <-a>, <-f>, <-u> и <-m>"; + КонецЕсли; + НастройкаУстановки.ИмяСервера = ПодключитьХаб(Хаб, Пул, МассивПакетовКУстановке); + КонецЕсли; + Если УстановкаВсехПакетов Тогда РаботаСПакетами.УстановитьВсеПакетыИзОблака(РежимУстановки, ЦелевойКаталог, НастройкаУстановки); ИначеЕсли ПустаяСтрока(ФайлПакетаУстановки) И ПустаяСтрока(ИнтернетСсылкаНаПакет) И МассивПакетовКУстановке.Количество() = 0 Тогда @@ -144,6 +159,29 @@ КонецПроцедуры +// Подключает пул хаба из опций --hub и --pool: пакеты и их зависимости ставятся с него. +// +// Параметры: +// Хаб - Строка - [схема://]хост[:порт]. +// Пул - Строка - пул хаба; пустой - пул хаба по умолчанию. +// ИменаПакетов - Массив - имена пакетов из аргумента PACKAGE. +// +// Возвращаемое значение: +// Строка - имя сервера пула в настройках. +// +Функция ПодключитьХаб(Знач Хаб, Знач Пул, Знач ИменаПакетов) Экспорт + + Адрес = АдресаХаба.Адрес(Хаб, Пул); + Для Каждого ИмяПакета Из ИменаПакетов Цикл + Если АдресаХаба.ЭтоАдресХаба(ИмяПакета) Тогда + ВызватьИсключение СтрШаблон("Пакет <%1> задан адресом вместе с опцией <--hub>: укажите одно из двух", ИмяПакета); + КонецЕсли; + КонецЦикла; + + Возврат РаботаСПакетами.ПодключитьСерверХаба(Адрес); + +КонецФункции + Функция ЭтоФайлПакета(Знач ИмяПакета) Возврат СтрЗаканчиваетсяНа(НРег(ИмяПакета), ".ospx"); diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index 30a4d62..0b0ea2e 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -10,9 +10,10 @@ Процедура ОписаниеКоманды(Знач КомандаПриложения) Экспорт - КомандаПриложения.Опция("hub", "", "Адрес хаба публикации: [схема://]хост[:порт][/пул]. - | Без пула - пул хаба по умолчанию.") + КомандаПриложения.Опция("hub", "", "Адрес хаба публикации: [схема://]хост[:порт].") .ВОкружении("OPM_HUB"); + КомандаПриложения.Опция("pool", "", "Пул хаба из опции <--hub>, без пула - пул хаба по умолчанию.") + .ВОкружении("OPM_POOL"); КомандаПриложения.Опция("token", "", "Токен авторизации в хабе пакетов: для OpenHub - персональный токен доступа, | для хаба без OpenHub - токен GitHub.com | - Токен авторизации создается на странице https://github.com/settings/tokens @@ -51,6 +52,7 @@ ИмяКаналаПубликации = КомандаПриложения.ЗначениеОпции("channel"); ИмяСервераПакетов = КомандаПриложения.ЗначениеОпции("mirror"); АдресХаба = КомандаПриложения.ЗначениеОпции("hub"); + Пул = КомандаПриложения.ЗначениеОпции("pool"); Если ЗначениеЗаполнено(ИмяСервераПакетов) Тогда Лог.Предупреждение("Опция <-m|--mirror> устарела и будет удалена. Используйте опцию <--hub>"); @@ -76,7 +78,7 @@ Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации, ПустаяСтрока(АдресХаба)); - ОтправитьПакетВХаб(ТокенАвторизации, ФайлПакета, Канал, СерверПубликации(АдресХаба, ИмяСервераПакетов)); + ОтправитьПакетВХаб(ТокенАвторизации, ФайлПакета, Канал, СерверПубликации(АдресХаба, ИмяСервераПакетов, Пул)); КонецПроцедуры @@ -143,19 +145,20 @@ КонецФункции -// Сервер публикации: адрес хаба из опции --hub, сервер из opm.cfg по имени либо хаб по умолчанию. +// Сервер публикации: пул хаба из опций --hub и --pool, сервер из opm.cfg по имени либо хаб по умолчанию. // // Параметры: -// АдресХаба - Строка - [схема://]хост[:порт][/пул]; пустой - публикация на сервер по имени. +// АдресХаба - Строка - [схема://]хост[:порт]; пустой - публикация на сервер по имени. // ИмяСервераПакетов - Строка - имя сервера из opm.cfg. +// Пул - Строка - пул хаба из АдресХаба; пустой - пул хаба по умолчанию. // // Возвращаемое значение: // СерверПакетов - сервер с ресурсом публикации. // -Функция СерверПубликации(Знач АдресХаба, Знач ИмяСервераПакетов) Экспорт +Функция СерверПубликации(Знач АдресХаба, Знач ИмяСервераПакетов, Знач Пул = "") Экспорт - Если ЗначениеЗаполнено(АдресХаба) Тогда - Возврат СерверыПакетов.ИзАдреса(АдресаХаба.АдресПула(АдресХаба)); + Если ЗначениеЗаполнено(АдресХаба) Или ЗначениеЗаполнено(Пул) Тогда + Возврат СерверыПакетов.ИзАдреса(АдресаХаба.Адрес(АдресХаба, Пул)); КонецЕсли; Для Каждого НастройкаСервера Из НастройкиOpm.ПолучитьНастройки().СервераПакетов Цикл diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" index 42b8511..69c142b 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" @@ -1,4 +1,4 @@ -// Адрес хаба в командной строке: [схема://]хост[:порт][/пул][/пакет[@спецификация]]. +// Адрес хаба в командной строке: [схема://]хост[:порт][/пул][/пакет[@спецификация]] либо хаб и пул по отдельности. Перем ФорматАдреса; @@ -44,22 +44,25 @@ КонецФункции -// Разбирает адрес пула, в который публикуют пакеты. +// Собирает адрес пула из хаба и пула, заданных по отдельности. // // Параметры: -// Адрес - Строка - [схема://]хост[:порт][/пул]. +// Хаб - Строка - [схема://]хост[:порт] без пути. +// Пул - Строка - имя пула; пустое - пул хаба по умолчанию. // // Возвращаемое значение: // Структура - Сервер (Строка - схема и хост), Порт (Число, 0 - порт схемы), // Пул (Строка, пустая - пул хаба по умолчанию). // -Функция АдресПула(Знач Адрес) Экспорт +Функция Адрес(Знач Хаб, Знач Пул = "") Экспорт - Результат = Разобрать(Адрес, "[схема://]хост[:порт][/пул]", 0, 1); - Если Результат.Сегменты.Количество() = 1 Тогда - Результат.Пул = Результат.Сегменты[0]; + Если ПустаяСтрока(Хаб) Тогда + ВызватьИсключение "Пул задаётся вместе с хабом: опция --pool требует опцию --hub"; КонецЕсли; + Результат = Разобрать(Хаб, "[схема://]хост[:порт] без пути, пул задаётся опцией --pool", 0, 0); + Результат.Пул = СокрЛП(Пул); + Возврат Результат; КонецФункции diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" index ab5d775..1126259 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" @@ -87,6 +87,24 @@ КонецФункции +// Добавляет в настройки сервер пула по адресу хаба: пакеты и их зависимости ставятся с него по имени сервера. +// +// Параметры: +// Адрес - Структура - результат АдресаХаба.АдресПакета или АдресаХаба.Адрес. +// +// Возвращаемое значение: +// Строка - имя сервера в настройках. +// +Функция ПодключитьСерверХаба(Знач Адрес) Экспорт + + Сервер = СерверыПакетов.ИзАдреса(Адрес); + НастройкиOpm.ДобавитьСерверПакетов(Сервер); + ВнутреннийМенеджерПолученияПакетов = Неопределено; + + Возврат Сервер.ПолучитьИмя(); + +КонецФункции + Процедура УстановитьПакетИзОблака(Знач ИмяПакета, Знач РежимУстановки, Знач ЦелевойКаталог, Знач НастройкаУстановки = Неопределено) Экспорт Лог.Отладка("Устанавливаю пакет <%1> из доступного хаба", ИмяПакета); @@ -99,10 +117,7 @@ // пакет и его зависимости ставятся с сервера из адреса Если АдресаХаба.ЭтоАдресХаба(ИмяПакета) Тогда Адрес = АдресаХаба.АдресПакета(ИмяПакета); - Сервер = СерверыПакетов.ИзАдреса(Адрес); - НастройкиOpm.ДобавитьСерверПакетов(Сервер); - ВнутреннийМенеджерПолученияПакетов = Неопределено; - ИмяСервера = Сервер.ПолучитьИмя(); + ИмяСервера = ПодключитьСерверХаба(Адрес); ИмяПакета = Адрес.Пакет; КонецЕсли; diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index d66e612..d036542 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -31,7 +31,7 @@ // из настроек с тем же адресом и портом. // // Параметры: -// Адрес - Структура - результат АдресаХаба.АдресПакета или АдресаХаба.АдресПула. +// Адрес - Структура - результат АдресаХаба.АдресПакета или АдресаХаба.Адрес. // // Возвращаемое значение: // СерверПакетов - сервер выдачи и публикации пула. diff --git a/tests/hub-address-test.os b/tests/hub-address-test.os index ca3c4a3..298718b 100644 --- a/tests/hub-address-test.os +++ b/tests/hub-address-test.os @@ -1,5 +1,6 @@ #Использовать asserts #Использовать "../src/core" +#Использовать "../src/cmd" Перем юТест; @@ -17,6 +18,8 @@ СписокТестов.Добавить("ТестДолжен_СобратьСерверПулаПоАдресу"); СписокТестов.Добавить("ТестДолжен_ВзятьАвторизациюАдресаИзНастроек"); СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетССервераИзАдреса"); + СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетСХабаИзОпций"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаАдресеПакетаВместеСОпциейХаба"); Возврат СписокТестов; @@ -69,7 +72,7 @@ Ожидаем.Что(АдресаХаба.АдресПакета("localhost/pkg").Сервер, "localhost").Равно("http://localhost"); Ожидаем.Что(АдресаХаба.АдресПакета("127.0.0.1/pkg").Сервер, "127.0.0.1").Равно("http://127.0.0.1"); Ожидаем.Что(АдресаХаба.АдресПакета("hub.example:3380/pkg").Сервер, "явный порт").Равно("http://hub.example"); - Ожидаем.Что(АдресаХаба.АдресПула("http://hub.example").Сервер, "явная схема").Равно("http://hub.example"); + Ожидаем.Что(АдресаХаба.Адрес("http://hub.example").Сервер, "явная схема").Равно("http://hub.example"); КонецПроцедуры @@ -80,14 +83,18 @@ .ВыбрасываетИсключение("ожидалось [схема://]хост[:порт]/[пул/]пакет[@спецификация]"); КонецЦикла; - Ожидаем.Что(АдресаХаба).Метод("АдресПула", ЗначенияПараметров("hub.example/corp/pkg")) - .ВыбрасываетИсключение("ожидалось [схема://]хост[:порт][/пул]"); + Ожидаем.Что(АдресаХаба).Метод("Адрес", ЗначенияПараметров("hub.example/corp")) + .ВыбрасываетИсключение("ожидалось [схема://]хост[:порт] без пути, пул задаётся опцией --pool"); + + Параметры = ЗначенияПараметров(""); + Параметры.Добавить("corp"); + Ожидаем.Что(АдресаХаба).Метод("Адрес", Параметры).ВыбрасываетИсключение("опция --pool требует опцию --hub"); КонецПроцедуры Процедура ТестДолжен_СобратьСерверПулаПоАдресу() Экспорт - Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.АдресПула("hub.example:3380/corp")); + Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.Адрес("hub.example:3380", "corp")); Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("http://hub.example:3380/corp"); Ожидаем.Что(Сервер.Сервер).Равно("http://hub.example"); @@ -95,7 +102,7 @@ Ожидаем.Что(Сервер.ПутьНаСервере).Равно("/api/v1/pools/corp/download/"); Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); - Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.АдресПула("hub.example")); + Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.Адрес("hub.example")); Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("https://hub.example"); Ожидаем.Что(Сервер.ПутьНаСервере, "пул по умолчанию").Равно("/download/"); @@ -135,6 +142,38 @@ КонецПроцедуры +Процедура ТестДолжен_УстанавливатьПакетСХабаИзОпций() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, + "fixtures", "ЗаглушкаСервераПакетов.os"))); + + НастройкаУстановки = РаботаСПакетами.ПолучитьНастройкуУстановки(); + НастройкаУстановки.ИмяСервера = Новый КомандаOpm_Install().ПодключитьХаб("127.0.0.1:1", "corp", + ЗначенияПараметров("pkg@1.0.0")); + + Ожидаем.Что(НастройкаУстановки.ИмяСервера).Равно("http://127.0.0.1:1/corp"); + Ожидаем.Что(НастройкиOpm.ПолучитьНастройки().СервераПакетов[1].ПутьНаСервере).Равно("/api/v1/pools/corp/download/"); + + Попытка + РаботаСПакетами.УстановитьПакетИзОблака("pkg@1.0.0", РежимУстановкиПакетов.Локально, + ОбъединитьПути(юТест.ИмяВременногоФайла(), "oscript_modules"), НастройкаУстановки); + ВызватьИсключение "установка без хаба должна отказать"; + Исключение + Ожидаем.Что(ОписаниеОшибки()).Содержит("Ошибка установки пакета pkg"); + КонецПопытки; + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаАдресеПакетаВместеСОпциейХаба() Экспорт + + Параметры = ЗначенияПараметров("hub.example"); + Параметры.Добавить(""); + Параметры.Добавить(ЗначенияПараметров("hub.example/corp/pkg")); + + Ожидаем.Что(Новый КомандаOpm_Install()).Метод("ПодключитьХаб", Параметры).ВыбрасываетИсключение("одно из двух"); + +КонецПроцедуры + Функция ЗначенияПараметров(Знач Значение) Параметры = Новый Массив; Параметры.Добавить(Значение); diff --git a/tests/push-target-test.os b/tests/push-target-test.os index 26f46e5..b982b6c 100644 --- a/tests/push-target-test.os +++ b/tests/push-target-test.os @@ -13,7 +13,8 @@ СписокТестов = Новый Массив; СписокТестов.Добавить("ТестДолжен_ПубликоватьВПулИзОпцииХаба"); - СписокТестов.Добавить("ТестДолжен_ОтказатьНаАдресеПакетаВОпцииХаба"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаПутиВОпцииХаба"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаПулеБезХаба"); СписокТестов.Добавить("ТестДолжен_ПубликоватьНаСерверИзНастроекПоИмени"); СписокТестов.Добавить("ТестДолжен_ПубликоватьВХабПоУмолчанию"); СписокТестов.Добавить("ТестДолжен_ОтправитьТокенИФайлВЗапросеПубликации"); @@ -36,7 +37,7 @@ Процедура ТестДолжен_ПубликоватьВПулИзОпцииХаба() Экспорт - Сервер = Пуш.СерверПубликации("127.0.0.1:3380/corp", ""); + Сервер = Пуш.СерверПубликации("127.0.0.1:3380", "", "corp"); Ожидаем.Что(Сервер.Сервер).Равно("http://127.0.0.1"); Ожидаем.Что(Сервер.Порт).Равно(3380); @@ -44,13 +45,24 @@ КонецПроцедуры -Процедура ТестДолжен_ОтказатьНаАдресеПакетаВОпцииХаба() Экспорт +Процедура ТестДолжен_ОтказатьНаПутиВОпцииХаба() Экспорт Параметры = Новый Массив; - Параметры.Добавить("hub.example/corp/pkg"); + Параметры.Добавить("hub.example/corp"); Параметры.Добавить(""); - Ожидаем.Что(Пуш).Метод("СерверПубликации", Параметры).ВыбрасываетИсключение("ожидалось [схема://]хост[:порт][/пул]"); + Ожидаем.Что(Пуш).Метод("СерверПубликации", Параметры).ВыбрасываетИсключение("без пути, пул задаётся опцией --pool"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаПулеБезХаба() Экспорт + + Параметры = Новый Массив; + Параметры.Добавить(""); + Параметры.Добавить(""); + Параметры.Добавить("corp"); + + Ожидаем.Что(Пуш).Метод("СерверПубликации", Параметры).ВыбрасываетИсключение("опция --pool требует опцию --hub"); КонецПроцедуры @@ -73,7 +85,7 @@ Процедура ТестДолжен_ОтправитьТокенИФайлВЗапросеПубликации() Экспорт - Сервер = Пуш.СерверПубликации("hub.example/corp", ""); + Сервер = Пуш.СерверПубликации("hub.example", "", "corp"); ФайлПакета = ФайлПакетаПробы(); Запрос = Сервер.ЗапросПубликации(ФайлПакета, "stable", "токен"); @@ -89,7 +101,7 @@ НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("corp", "https://hub.example", , , 0, 0, "Bearer из настроек")); - Запрос = Пуш.СерверПубликации("hub.example/corp", "").ЗапросПубликации(ФайлПакетаПробы(), "", "токен"); + Запрос = Пуш.СерверПубликации("hub.example", "", "corp").ЗапросПубликации(ФайлПакетаПробы(), "", "токен"); Ожидаем.Что(Запрос.Заголовки["Authorization"]).Равно("Bearer из настроек"); Ожидаем.Что(Запрос.Заголовки["OAUTH-TOKEN"]).Равно(Неопределено); From 9202b91a10de42620afbe45819331ed553639861 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 08:29:26 +0400 Subject: [PATCH 11/17] =?UTF-8?q?=D0=9F=D1=83=D0=BB=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=20=D1=85=D0=B0=D0=B1=D0=B0:=20--pool=20=D0=B1=D0=B5=D0=B7=20--?= =?UTF-8?q?hub=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D1=83=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B8=20=D1=81=D1=82=D0=B0=D0=B2=D0=B8=D1=82=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=BF=D1=83=D0=BB=D0=B0=20=D1=85=D0=B0=D0=B1=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=D0=BD=D0=B8=D1=8E?= =?UTF-8?q?=20(http://hub.oscript.io),=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B5=D1=82;=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20--pool=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=8F?= =?UTF-8?q?=D0=B7=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=BD,=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BA=20=D0=BF=D1=80=D0=B8=20--hub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++-- ...74\320\260\320\275\320\264\320\260Opm_Install.os" | 4 ++-- ...0\274\320\260\320\275\320\264\320\260Opm_Push.os" | 8 ++++---- ...1\201\320\260\320\245\320\260\320\261\320\260.os" | 4 ++-- tests/hub-address-test.os | 6 +++--- tests/push-target-test.os | 12 +++++------- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 89e582d..d0bf4d2 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ OneScript Package Manager # Адрес хаба прямо в команде Установка принимает адрес пакета в хабе вместо имени либо хаб опцией `--hub` и пул опцией `--pool` -(переменные окружения `OPM_HUB` и `OPM_POOL`), публикация — только опциями; запись в `opm.cfg` не нужна: +(переменные окружения `OPM_HUB` и `OPM_POOL`), публикация — только опциями; без `--hub` — хаб по умолчанию, +без `--pool` — пул хаба по умолчанию; запись в `opm.cfg` не нужна: ```sh opm install localhost:3380/corp/my-package@stable # хост, порт, пул, пакет, тег @@ -37,7 +38,7 @@ opm push --hub https://hub.example.com --token <токен> my-package-1.0.0.osp * В адресе пакета последний сегмент пути — пакет, сегмент перед ним — пул; без пула — пул хаба по умолчанию. Зависимости пакета ставятся с того же сервера, у `install --hub` — с хаба из опций. -* Отказ: путь в `--hub`, `--pool` без `--hub`, адрес пакета вместе с `--hub`, `install --hub` вместе с `-a`, `-f`, `-u`, `-m`. +* Отказ: путь в `--hub`, адрес пакета вместе с `--hub`, `install --hub` вместе с `-a`, `-f`, `-u`, `-m`. * Строка считается адресом, если в ней есть схема либо путь после хоста с портом, с точкой в имени или `localhost`. Остальное — имя пакета. * Без схемы берётся `http` для `localhost`, `127.*` и адреса с явным портом, иначе `https`. diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" index 9a55dea..77a6b91 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" @@ -23,9 +23,9 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Опция("hub", "", "Адрес хаба, с которого ставятся пакеты и их зависимости: [схема://]хост[:порт].") + КомандаПриложения.Опция("hub", "", "Адрес хаба, с которого ставятся пакеты и их зависимости: [схема://]хост[:порт]; без адреса - хаб по умолчанию.") .ВОкружении("OPM_HUB"); - КомандаПриложения.Опция("pool", "", "Пул хаба из опции <--hub>, без пула - пул хаба по умолчанию.") + КомандаПриложения.Опция("pool", "", "Пул хаба; без пула - пул хаба по умолчанию.") .ВОкружении("OPM_POOL"); КомандаПриложения.Аргумент("PACKAGE", "", ПараметрыПриложенияOpm.ОписаниеАргументаПакет()) diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index 0b0ea2e..a0d0261 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -12,7 +12,7 @@ КомандаПриложения.Опция("hub", "", "Адрес хаба публикации: [схема://]хост[:порт].") .ВОкружении("OPM_HUB"); - КомандаПриложения.Опция("pool", "", "Пул хаба из опции <--hub>, без пула - пул хаба по умолчанию.") + КомандаПриложения.Опция("pool", "", "Пул хаба; без пула - пул хаба по умолчанию.") .ВОкружении("OPM_POOL"); КомандаПриложения.Опция("token", "", "Токен авторизации в хабе пакетов: для OpenHub - персональный токен доступа, | для хаба без OpenHub - токен GitHub.com @@ -76,7 +76,7 @@ ФайлПакета = ПолучитьФайлПакета(МаскаФайлаПакета); - Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации, ПустаяСтрока(АдресХаба)); + Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации, ПустаяСтрока(АдресХаба) И ПустаяСтрока(Пул)); ОтправитьПакетВХаб(ТокенАвторизации, ФайлПакета, Канал, СерверПубликации(АдресХаба, ИмяСервераПакетов, Пул)); @@ -145,10 +145,10 @@ КонецФункции -// Сервер публикации: пул хаба из опций --hub и --pool, сервер из opm.cfg по имени либо хаб по умолчанию. +// Сервер публикации: пул хаба из опций --hub и --pool (пустой хаб - хаб по умолчанию), сервер из opm.cfg по имени либо хаб по умолчанию. // // Параметры: -// АдресХаба - Строка - [схема://]хост[:порт]; пустой - публикация на сервер по имени. +// АдресХаба - Строка - [схема://]хост[:порт]; пустой без пула - публикация на сервер по имени. // ИмяСервераПакетов - Строка - имя сервера из opm.cfg. // Пул - Строка - пул хаба из АдресХаба; пустой - пул хаба по умолчанию. // diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" index 69c142b..6af4947 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" @@ -47,7 +47,7 @@ // Собирает адрес пула из хаба и пула, заданных по отдельности. // // Параметры: -// Хаб - Строка - [схема://]хост[:порт] без пути. +// Хаб - Строка - [схема://]хост[:порт] без пути; пустой - хаб по умолчанию. // Пул - Строка - имя пула; пустое - пул хаба по умолчанию. // // Возвращаемое значение: @@ -57,7 +57,7 @@ Функция Адрес(Знач Хаб, Знач Пул = "") Экспорт Если ПустаяСтрока(Хаб) Тогда - ВызватьИсключение "Пул задаётся вместе с хабом: опция --pool требует опцию --hub"; + Хаб = КонстантыOpm.СерверУдаленногоХранилища; КонецЕсли; Результат = Разобрать(Хаб, "[схема://]хост[:порт] без пути, пул задаётся опцией --pool", 0, 0); diff --git a/tests/hub-address-test.os b/tests/hub-address-test.os index 298718b..c237f31 100644 --- a/tests/hub-address-test.os +++ b/tests/hub-address-test.os @@ -86,9 +86,9 @@ Ожидаем.Что(АдресаХаба).Метод("Адрес", ЗначенияПараметров("hub.example/corp")) .ВыбрасываетИсключение("ожидалось [схема://]хост[:порт] без пути, пул задаётся опцией --pool"); - Параметры = ЗначенияПараметров(""); - Параметры.Добавить("corp"); - Ожидаем.Что(АдресаХаба).Метод("Адрес", Параметры).ВыбрасываетИсключение("опция --pool требует опцию --hub"); + Адрес = АдресаХаба.Адрес("", "corp"); + Ожидаем.Что(Адрес.Сервер, "хаб по умолчанию").Равно("http://hub.oscript.io"); + Ожидаем.Что(Адрес.Пул).Равно("corp"); КонецПроцедуры diff --git a/tests/push-target-test.os b/tests/push-target-test.os index b982b6c..cadb233 100644 --- a/tests/push-target-test.os +++ b/tests/push-target-test.os @@ -14,7 +14,7 @@ СписокТестов.Добавить("ТестДолжен_ПубликоватьВПулИзОпцииХаба"); СписокТестов.Добавить("ТестДолжен_ОтказатьНаПутиВОпцииХаба"); - СписокТестов.Добавить("ТестДолжен_ОтказатьНаПулеБезХаба"); + СписокТестов.Добавить("ТестДолжен_ПубликоватьВПулХабаПоУмолчанию"); СписокТестов.Добавить("ТестДолжен_ПубликоватьНаСерверИзНастроекПоИмени"); СписокТестов.Добавить("ТестДолжен_ПубликоватьВХабПоУмолчанию"); СписокТестов.Добавить("ТестДолжен_ОтправитьТокенИФайлВЗапросеПубликации"); @@ -55,14 +55,12 @@ КонецПроцедуры -Процедура ТестДолжен_ОтказатьНаПулеБезХаба() Экспорт +Процедура ТестДолжен_ПубликоватьВПулХабаПоУмолчанию() Экспорт - Параметры = Новый Массив; - Параметры.Добавить(""); - Параметры.Добавить(""); - Параметры.Добавить("corp"); + Сервер = Пуш.СерверПубликации("", "", "corp"); - Ожидаем.Что(Пуш).Метод("СерверПубликации", Параметры).ВыбрасываетИсключение("опция --pool требует опцию --hub"); + Ожидаем.Что(Сервер.Сервер).Равно("http://hub.oscript.io"); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); КонецПроцедуры From 494d09bf6155fcdc784124daf80b0bc616909926 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 11:18:33 +0400 Subject: [PATCH 12/17] =?UTF-8?q?=D0=AD=D0=BA=D1=81=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=82=20=D1=80=D0=B0=D0=B4=D0=B8=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D1=81=D0=BD=D1=8F=D1=82:=20=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=E2=80=94=20=D0=A1=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=8B=D0=9F=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2.=D0=94?= =?UTF-8?q?=D0=BB=D1=8F=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8,=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=20=D1=85=D0=B0?= =?UTF-8?q?=D0=B1=D0=B0=20=D0=B8=20=D0=B5=D0=B3=D0=BE=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=E2=80=94=20=D0=B2=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=80=D0=B8=20=D0=A1=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=9F=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2.=D0=9E=D1=82=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=D0=9F=D0=B0=D0=BA=D0=B5?= =?UTF-8?q?=D1=82,=20=D0=9F=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=D0=A5=D0=B0=D0=B1=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD?= =?UTF-8?q?=D0=B4=D1=8B=20install=20=D0=B1=D0=B5=D0=B7=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D1=80=D1=82=D0=B0;=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=84=D0=BB=D0=B8=D0=BA=D1=82=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20=D1=81=20--hub?= =?UTF-8?q?=20=E2=80=94=20=D1=84=D0=B8=D1=87=D0=B5=D0=B9=20install-hub;=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20=E2=80=94=20=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=20=D0=B8=20?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=83=D1=80=D1=81=20=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B0,=20=D0=B1=D0=B5=D0=B7=20=D0=B7=D0=B0=D0=B3?= =?UTF-8?q?=D0=BB=D1=83=D1=88=D0=BA=D0=B8=20=D1=85=D0=B0=D0=B1=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20python?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/install-hub.feature | 20 +++++ ...260\320\275\320\264\320\260Opm_Install.os" | 2 +- ...20\260\320\275\320\264\320\260Opm_Push.os" | 83 +------------------ ...20\272\320\265\321\202\320\276\320\262.os" | 51 ++++++++---- ...20\272\320\265\321\202\320\276\320\262.os" | 28 +++++++ ...20\272\320\265\321\202\320\276\320\262.os" | 4 - tests/hub-address-test.os | 15 +--- tests/push-target-test.os | 83 ++----------------- 8 files changed, 94 insertions(+), 192 deletions(-) create mode 100644 features/install-hub.feature diff --git a/features/install-hub.feature b/features/install-hub.feature new file mode 100644 index 0000000..6fa3d0a --- /dev/null +++ b/features/install-hub.feature @@ -0,0 +1,20 @@ +# language: ru + +Функциональность: Установка с хаба из опций + +Как разработчик + Я хочу ставить пакеты с хаба, названного опцией --hub + Чтобы не путать адрес хаба в опции с адресом пакета в аргументе + +Контекст: Тестовый контекст + Допустим Я очищаю параметры команды "opm" в контексте + И Я устанавливаю путь выполнения команды "opm" к текущей библиотеке + +Сценарий: Адрес пакета вместе с опцией --hub + Допустим Я добавляю параметр "install" для команды "opm" + И Я добавляю параметр "--hub" для команды "opm" + И Я добавляю параметр "hub.example" для команды "opm" + И Я добавляю параметр "hub.example/corp/pkg" для команды "opm" + Когда Я выполняю команду "opm" + Тогда Вывод команды "opm" содержит "задан адресом вместе с опцией <--hub>: укажите одно из двух" + И Код возврата команды "opm" равен 1 diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" index 77a6b91..f8ef647 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" @@ -169,7 +169,7 @@ // Возвращаемое значение: // Строка - имя сервера пула в настройках. // -Функция ПодключитьХаб(Знач Хаб, Знач Пул, Знач ИменаПакетов) Экспорт +Функция ПодключитьХаб(Знач Хаб, Знач Пул, Знач ИменаПакетов) Адрес = АдресаХаба.Адрес(Хаб, Пул); Для Каждого ИмяПакета Из ИменаПакетов Цикл diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index a0d0261..17aa0f8 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -78,7 +78,9 @@ Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации, ПустаяСтрока(АдресХаба) И ПустаяСтрока(Пул)); - ОтправитьПакетВХаб(ТокенАвторизации, ФайлПакета, Канал, СерверПубликации(АдресХаба, ИмяСервераПакетов, Пул)); + Сервер = СерверыПакетов.ДляПубликации(АдресХаба, ИмяСервераПакетов, Пул); + Лог.Отладка("Публикация на сервер %1", Сервер.ПолучитьИмя()); + Лог.Информация(Сервер.ОтправитьПакет(ФайлПакета, Канал, ТокенАвторизации)); КонецПроцедуры @@ -145,85 +147,6 @@ КонецФункции -// Сервер публикации: пул хаба из опций --hub и --pool (пустой хаб - хаб по умолчанию), сервер из opm.cfg по имени либо хаб по умолчанию. -// -// Параметры: -// АдресХаба - Строка - [схема://]хост[:порт]; пустой без пула - публикация на сервер по имени. -// ИмяСервераПакетов - Строка - имя сервера из opm.cfg. -// Пул - Строка - пул хаба из АдресХаба; пустой - пул хаба по умолчанию. -// -// Возвращаемое значение: -// СерверПакетов - сервер с ресурсом публикации. -// -Функция СерверПубликации(Знач АдресХаба, Знач ИмяСервераПакетов, Знач Пул = "") Экспорт - - Если ЗначениеЗаполнено(АдресХаба) Или ЗначениеЗаполнено(Пул) Тогда - Возврат СерверыПакетов.ИзАдреса(АдресаХаба.Адрес(АдресХаба, Пул)); - КонецЕсли; - - Для Каждого НастройкаСервера Из НастройкиOpm.ПолучитьНастройки().СервераПакетов Цикл - Если СтрСравнить(НастройкаСервера.Имя, ИмяСервераПакетов) = 0 Тогда - Возврат НастройкаСервера; - КонецЕсли; - КонецЦикла; - - Возврат Новый СерверПакетов(КонстантыOpm.СерверУдаленногоХранилища, КонстантыOpm.СерверУдаленногоХранилища, , - КонстантыOpm.РесурсПубликацииПакетов, 0, 0, , , 600); - -КонецФункции - -// Отправляет файл пакета в хаб; отказ хаба - исключение с его сообщением. -// -// Параметры: -// ТокенАвторизации - Строка - токен публикации. -// ФайлПакета - Файл - файл .ospx. -// Канал - Строка - канал публикации. -// Сервер - СерверПакетов - сервер публикации. -// -Процедура ОтправитьПакетВХаб(Знач ТокенАвторизации, Знач ФайлПакета, Знач Канал, Знач Сервер) Экспорт - - Лог.Отладка("Публикация на сервер %1", Сервер.ПолучитьИмя()); - - Ответ = Сервер.ОтправитьПакет(ФайлПакета, Канал, ТокенАвторизации); - ТелоОтвета = СообщениеХаба(Ответ.ПолучитьТелоКакСтроку()); - - Если Не КодУспешнойОтправки(Ответ.КодСостояния) Тогда - ВызватьИсключение ТелоОтвета; - КонецЕсли; - - Лог.Информация(ТелоОтвета); - -КонецПроцедуры - -// Хаб OpenHub отвечает JSON {"КодСостояние", "Сообщение"}: пользователю - только сообщение. -Функция СообщениеХаба(Знач ТелоОтвета) - - Попытка - Чтение = Новый ЧтениеJSON(); - Чтение.УстановитьСтроку(ТелоОтвета); - Ответ = ПрочитатьJSON(Чтение, Истина); - Чтение.Закрыть(); - Исключение - Возврат ТелоОтвета; - КонецПопытки; - - Если ТипЗнч(Ответ) = Тип("Соответствие") И Ответ["Сообщение"] <> Неопределено Тогда - Возврат Ответ["Сообщение"]; - КонецЕсли; - - Возврат ТелоОтвета; - -КонецФункции - -Функция КодУспешнойОтправки(Знач КодСостояния) - - Возврат КодСостояния = 200 // OK - Или КодСостояния = 201 // CREATED - Или КодСостояния = 202 // ACCEPTED - ; - -КонецФункции - Функция ДопустимыеИменаКаналовЗначения() ДопустимыеИменаКаналовЗначения = Новый Массив; diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index f3609c9..154a2c0 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -123,7 +123,7 @@ КонецФункции -// Отправляет файл пакета на ресурс публикации. +// Публикует файл пакета; отказ сервера - исключение с его сообщением. // // Параметры: // ФайлПакета - Файл - файл .ospx. @@ -131,7 +131,7 @@ // Токен - Строка - токен публикации; пустой - без токена. // // Возвращаемое значение: -// HTTPОтвет - ответ сервера. +// Строка - сообщение сервера. // Функция ОтправитьПакет(Знач ФайлПакета, Знач Канал, Знач Токен) Экспорт @@ -139,26 +139,23 @@ Запрос = ЗапросПубликации(ФайлПакета, Канал, Токен); Попытка - Возврат Соединение.ОтправитьДляОбработки(Запрос); + Ответ = Соединение.ОтправитьДляОбработки(Запрос); Исключение Лог.Отладка("Публикация в %1%2 не удалась: %3", Сервер, РесурсПубликацииПакетов, ОписаниеОшибки()); ВызватьИсключение СтрШаблон("Не удалось подключиться к %1", АдресДляСообщений()); КонецПопытки; + Сообщение = СообщениеСервера(Ответ.ПолучитьТелоКакСтроку()); + Если Не КодУспешнойОтправки(Ответ.КодСостояния) Тогда + ВызватьИсключение Сообщение; + КонецЕсли; + + Возврат Сообщение; + КонецФункции -// Запрос публикации файла пакета. Токен уходит заголовком OAUTH-TOKEN, -// если у сервера нет своей авторизации. -// -// Параметры: -// ФайлПакета - Файл - файл .ospx. -// Канал - Строка - значение заголовка CHANNEL. -// Токен - Строка - токен публикации; пустой - без токена. -// -// Возвращаемое значение: -// HTTPЗапрос - запрос с заголовками и телом. -// -Функция ЗапросПубликации(Знач ФайлПакета, Знач Канал, Знач Токен) Экспорт +// Токен уходит заголовком OAUTH-TOKEN, если у сервера нет своей авторизации. +Функция ЗапросПубликации(Знач ФайлПакета, Знач Канал, Знач Токен) Запрос = Новый HTTPЗапрос(РесурсПубликацииПакетов); Запрос.Заголовки.Вставить("FILE-NAME", ФайлПакета.Имя); @@ -173,6 +170,30 @@ КонецФункции +// Хаб OpenHub отвечает JSON {"КодСостояние", "Сообщение"}: пользователю - только сообщение. +Функция СообщениеСервера(Знач ТелоОтвета) + + Попытка + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(ТелоОтвета); + Ответ = ПрочитатьJSON(Чтение, Истина); + Чтение.Закрыть(); + Исключение + Возврат ТелоОтвета; + КонецПопытки; + + Если ТипЗнч(Ответ) = Тип("Соответствие") И Ответ["Сообщение"] <> Неопределено Тогда + Возврат Ответ["Сообщение"]; + КонецЕсли; + + Возврат ТелоОтвета; + +КонецФункции + +Функция КодУспешнойОтправки(Знач КодСостояния) + Возврат КодСостояния = 200 Или КодСостояния = 201 Или КодСостояния = 202; +КонецФункции + // Платформа оставляет плюс в адресе как есть, а в строке запроса он читается пробелом. Функция ЧастьАдреса(Знач Значение) Возврат КодироватьСтроку(Значение, СпособКодированияСтроки.КодировкаURL); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index d036542..ce48e3a 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -61,6 +61,34 @@ КонецФункции +// Сервер публикации: пул хаба по адресу и пулу (пустой адрес - хаб по умолчанию), +// сервер из настроек по имени либо хаб по умолчанию. +// +// Параметры: +// АдресХаба - Строка - [схема://]хост[:порт]; пустой без пула - публикация на сервер по имени. +// ИмяСервераПакетов - Строка - имя сервера из настроек. +// Пул - Строка - пул хаба из АдресХаба; пустой - пул хаба по умолчанию. +// +// Возвращаемое значение: +// СерверПакетов - сервер с ресурсом публикации. +// +Функция ДляПубликации(Знач АдресХаба, Знач ИмяСервераПакетов, Знач Пул = "") Экспорт + + Если ЗначениеЗаполнено(АдресХаба) Или ЗначениеЗаполнено(Пул) Тогда + Возврат ИзАдреса(АдресаХаба.Адрес(АдресХаба, Пул)); + КонецЕсли; + + Для Каждого НастройкаСервера Из НастройкиOpm.ПолучитьНастройки().СервераПакетов Цикл + Если СтрСравнить(НастройкаСервера.Имя, ИмяСервераПакетов) = 0 Тогда + Возврат НастройкаСервера; + КонецЕсли; + КонецЦикла; + + Возврат Новый СерверПакетов(КонстантыOpm.СерверУдаленногоХранилища, КонстантыOpm.СерверУдаленногоХранилища, , + КонстантыOpm.РесурсПубликацииПакетов, 0, 0, , , 600); + +КонецФункции + Функция ОсновнойСервер() Экспорт Возврат Новый СерверПакетов("ОсновнойСерверПакетов", КонстантыOpm.СерверУдаленногоХранилища, diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 0c4486e..3a13529 100644 --- "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -40,7 +40,3 @@ Запросы.Добавить(Новый Структура("ИмяРесурса, Спецификация", ИмяРесурса, Спецификация)); Возврат Ответ; КонецФункции - -Функция ОтправитьПакет(Знач ФайлПакета, Знач Канал, Знач Токен) Экспорт - Возврат Ответ; -КонецФункции diff --git a/tests/hub-address-test.os b/tests/hub-address-test.os index c237f31..b8e916b 100644 --- a/tests/hub-address-test.os +++ b/tests/hub-address-test.os @@ -1,6 +1,5 @@ #Использовать asserts #Использовать "../src/core" -#Использовать "../src/cmd" Перем юТест; @@ -19,7 +18,6 @@ СписокТестов.Добавить("ТестДолжен_ВзятьАвторизациюАдресаИзНастроек"); СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетССервераИзАдреса"); СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетСХабаИзОпций"); - СписокТестов.Добавить("ТестДолжен_ОтказатьНаАдресеПакетаВместеСОпциейХаба"); Возврат СписокТестов; @@ -148,8 +146,7 @@ "fixtures", "ЗаглушкаСервераПакетов.os"))); НастройкаУстановки = РаботаСПакетами.ПолучитьНастройкуУстановки(); - НастройкаУстановки.ИмяСервера = Новый КомандаOpm_Install().ПодключитьХаб("127.0.0.1:1", "corp", - ЗначенияПараметров("pkg@1.0.0")); + НастройкаУстановки.ИмяСервера = РаботаСПакетами.ПодключитьСерверХаба(АдресаХаба.Адрес("127.0.0.1:1", "corp")); Ожидаем.Что(НастройкаУстановки.ИмяСервера).Равно("http://127.0.0.1:1/corp"); Ожидаем.Что(НастройкиOpm.ПолучитьНастройки().СервераПакетов[1].ПутьНаСервере).Равно("/api/v1/pools/corp/download/"); @@ -164,16 +161,6 @@ КонецПроцедуры -Процедура ТестДолжен_ОтказатьНаАдресеПакетаВместеСОпциейХаба() Экспорт - - Параметры = ЗначенияПараметров("hub.example"); - Параметры.Добавить(""); - Параметры.Добавить(ЗначенияПараметров("hub.example/corp/pkg")); - - Ожидаем.Что(Новый КомандаOpm_Install()).Метод("ПодключитьХаб", Параметры).ВыбрасываетИсключение("одно из двух"); - -КонецПроцедуры - Функция ЗначенияПараметров(Знач Значение) Параметры = Новый Массив; Параметры.Добавить(Значение); diff --git a/tests/push-target-test.os b/tests/push-target-test.os index cadb233..dd9bd4e 100644 --- a/tests/push-target-test.os +++ b/tests/push-target-test.os @@ -1,15 +1,8 @@ #Использовать asserts -#Использовать tempfiles #Использовать "../src/core" -#Использовать "../src/cmd" - -Перем юТест; -Перем Пуш; Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт - юТест = Тестирование; - СписокТестов = Новый Массив; СписокТестов.Добавить("ТестДолжен_ПубликоватьВПулИзОпцииХаба"); @@ -17,27 +10,22 @@ СписокТестов.Добавить("ТестДолжен_ПубликоватьВПулХабаПоУмолчанию"); СписокТестов.Добавить("ТестДолжен_ПубликоватьНаСерверИзНастроекПоИмени"); СписокТестов.Добавить("ТестДолжен_ПубликоватьВХабПоУмолчанию"); - СписокТестов.Добавить("ТестДолжен_ОтправитьТокенИФайлВЗапросеПубликации"); - СписокТестов.Добавить("ТестДолжен_НеОтправлятьТокенСерверуСоСвоейАвторизацией"); - СписокТестов.Добавить("ТестДолжен_ПоказатьСообщениеХабаПриОтказе"); Возврат СписокТестов; КонецФункции Процедура ПередЗапускомТеста() Экспорт - Пуш = Новый КомандаOpm_Push(); НастройкиOpm.СброситьНастройки(); КонецПроцедуры Процедура ПослеЗапускаТеста() Экспорт НастройкиOpm.СброситьНастройки(); - ВременныеФайлы.Удалить(); КонецПроцедуры Процедура ТестДолжен_ПубликоватьВПулИзОпцииХаба() Экспорт - Сервер = Пуш.СерверПубликации("127.0.0.1:3380", "", "corp"); + Сервер = СерверыПакетов.ДляПубликации("127.0.0.1:3380", "", "corp"); Ожидаем.Что(Сервер.Сервер).Равно("http://127.0.0.1"); Ожидаем.Что(Сервер.Порт).Равно(3380); @@ -51,13 +39,13 @@ Параметры.Добавить("hub.example/corp"); Параметры.Добавить(""); - Ожидаем.Что(Пуш).Метод("СерверПубликации", Параметры).ВыбрасываетИсключение("без пути, пул задаётся опцией --pool"); + Ожидаем.Что(СерверыПакетов).Метод("ДляПубликации", Параметры).ВыбрасываетИсключение("без пути, пул задаётся опцией --pool"); КонецПроцедуры Процедура ТестДолжен_ПубликоватьВПулХабаПоУмолчанию() Экспорт - Сервер = Пуш.СерверПубликации("", "", "corp"); + Сервер = СерверыПакетов.ДляПубликации("", "", "corp"); Ожидаем.Что(Сервер.Сервер).Равно("http://hub.oscript.io"); Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); @@ -68,76 +56,15 @@ НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("corp", "https://hub.example", , "/api/v1/pools/corp/push")); - Ожидаем.Что(Пуш.СерверПубликации("", "corp").РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); + Ожидаем.Что(СерверыПакетов.ДляПубликации("", "corp").РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); КонецПроцедуры Процедура ТестДолжен_ПубликоватьВХабПоУмолчанию() Экспорт - Сервер = Пуш.СерверПубликации("", ""); + Сервер = СерверыПакетов.ДляПубликации("", ""); Ожидаем.Что(Сервер.Сервер).Равно(КонстантыOpm.СерверУдаленногоХранилища); Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно(КонстантыOpm.РесурсПубликацииПакетов); КонецПроцедуры - -Процедура ТестДолжен_ОтправитьТокенИФайлВЗапросеПубликации() Экспорт - - Сервер = Пуш.СерверПубликации("hub.example", "", "corp"); - ФайлПакета = ФайлПакетаПробы(); - - Запрос = Сервер.ЗапросПубликации(ФайлПакета, "stable", "токен"); - - Ожидаем.Что(Запрос.АдресРесурса).Равно("/api/v1/pools/corp/push"); - Ожидаем.Что(Запрос.Заголовки["FILE-NAME"]).Равно(ФайлПакета.Имя); - Ожидаем.Что(Запрос.Заголовки["CHANNEL"]).Равно("stable"); - Ожидаем.Что(Запрос.Заголовки["OAUTH-TOKEN"]).Равно("токен"); - -КонецПроцедуры - -Процедура ТестДолжен_НеОтправлятьТокенСерверуСоСвоейАвторизацией() Экспорт - - НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("corp", "https://hub.example", , , 0, 0, "Bearer из настроек")); - - Запрос = Пуш.СерверПубликации("hub.example", "", "corp").ЗапросПубликации(ФайлПакетаПробы(), "", "токен"); - - Ожидаем.Что(Запрос.Заголовки["Authorization"]).Равно("Bearer из настроек"); - Ожидаем.Что(Запрос.Заголовки["OAUTH-TOKEN"]).Равно(Неопределено); - -КонецПроцедуры - -Процедура ТестДолжен_ПоказатьСообщениеХабаПриОтказе() Экспорт - - Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); - Ответ.КодСостояния = 409; - Ответ.Тело = "{""КодСостояние"": 409, ""Сообщение"": ""Версия 1.0.0 уже опубликована""}"; - Сервер = ЗагрузитьЗаглушку("ЗаглушкаСервераПакетов.os"); - Сервер.Отвечать(Ответ); - - Параметры = Новый Массив; - Параметры.Добавить("токен"); - Параметры.Добавить(ФайлПакетаПробы()); - Параметры.Добавить(""); - Параметры.Добавить(Сервер); - - Ожидаем.Что(Пуш).Метод("ОтправитьПакетВХаб", Параметры).ВыбрасываетИсключение("Версия 1.0.0 уже опубликована"); - - Ответ.Тело = "Bad Gateway"; - Ожидаем.Что(Пуш).Метод("ОтправитьПакетВХаб", Параметры).ВыбрасываетИсключение("Bad Gateway"); - -КонецПроцедуры - -Функция ФайлПакетаПробы() - - ПутьКФайлу = ВременныеФайлы.НовоеИмяФайла("ospx"); - Запись = Новый ЗаписьТекста(ПутьКФайлу); - Запись.Записать("содержимое пакета"); - Запись.Закрыть(); - - Возврат Новый Файл(ПутьКФайлу); - -КонецФункции - -Функция ЗагрузитьЗаглушку(Знач ИмяФайла) - Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); -КонецФункции From 11f9987fee9599c693fcd398abf644284f93ed7a Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 12:25:45 +0400 Subject: [PATCH 13/17] =?UTF-8?q?push=20--force:=20=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D1=83=D1=80=D1=81=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=20for?= =?UTF-8?q?ce=3D1=20(=D0=A1=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D1=8B=D0=9F?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2.=D0=94=D0=BB=D1=8F=D0=9F?= =?UTF-8?q?=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20=D1=81?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D0=B7=D0=BD=D0=B0=D0=BA=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=A4=D0=BE=D1=80=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D1=8C),=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D1=82=D1=8C=20=D0=BB=D0=B8=20=D0=B7=D0=B0=D0=BD?= =?UTF-8?q?=D1=8F=D1=82=D1=8B=D0=B9=20=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8,=20=D1=80=D0=B5=D1=88?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D1=85=D0=B0=D0=B1;=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D1=8B=20=E2=80=94=20=D0=BF=D0=BE=20=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D1=83=D1=80=D1=81=D1=83=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ ...20\260\320\275\320\264\320\260Opm_Push.os" | 4 +++- ...20\272\320\265\321\202\320\276\320\262.os" | 16 +++++++++++++- tests/push-target-test.os | 22 +++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d0bf4d2..4673205 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ opm push --hub https://hub.example.com --token <токен> my-package-1.0.0.osp * `opm push --hub` отправляет токен из опции `--token` (переменная окружения `GITHUB_OAUTH_TOKEN`) или из настройки `ТокенПубликацииПакетов`. Канал при публикации в такой хаб необязателен. * Опция `-m|--mirror` у `push` устарела и печатает предупреждение: вместо неё — `--hub`. +* `opm push --force` просит хаб опубликовать поверх уже занятого номера версии (параметр `force=1` + в запросе публикации); перезапишет ли он версию, решает хаб. # Версия пакета после @ diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index 17aa0f8..0dd767b 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -22,6 +22,7 @@ .ВОкружении("GITHUB_OAUTH_TOKEN"); ; КомандаПриложения.Опция("f file", "", "Маска или имя файла пакета."); + КомандаПриложения.Опция("force", Ложь, "Опубликовать поверх уже занятого номера версии."); КомандаПриложения.Опция("c channel", "auto", "Канал публикации.") .ТПеречисление() .Перечисление("auto", "auto", "Автоматическое определение канала. @@ -53,6 +54,7 @@ ИмяСервераПакетов = КомандаПриложения.ЗначениеОпции("mirror"); АдресХаба = КомандаПриложения.ЗначениеОпции("hub"); Пул = КомандаПриложения.ЗначениеОпции("pool"); + Форсировать = КомандаПриложения.ЗначениеОпции("force"); Если ЗначениеЗаполнено(ИмяСервераПакетов) Тогда Лог.Предупреждение("Опция <-m|--mirror> устарела и будет удалена. Используйте опцию <--hub>"); @@ -78,7 +80,7 @@ Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации, ПустаяСтрока(АдресХаба) И ПустаяСтрока(Пул)); - Сервер = СерверыПакетов.ДляПубликации(АдресХаба, ИмяСервераПакетов, Пул); + Сервер = СерверыПакетов.ДляПубликации(АдресХаба, ИмяСервераПакетов, Пул, Форсировать); Лог.Отладка("Публикация на сервер %1", Сервер.ПолучитьИмя()); Лог.Информация(Сервер.ОтправитьПакет(ФайлПакета, Канал, ТокенАвторизации)); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index ce48e3a..b2769e3 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -68,11 +68,25 @@ // АдресХаба - Строка - [схема://]хост[:порт]; пустой без пула - публикация на сервер по имени. // ИмяСервераПакетов - Строка - имя сервера из настроек. // Пул - Строка - пул хаба из АдресХаба; пустой - пул хаба по умолчанию. +// Форсировать - Булево - опубликовать поверх уже занятого номера версии. // // Возвращаемое значение: // СерверПакетов - сервер с ресурсом публикации. // -Функция ДляПубликации(Знач АдресХаба, Знач ИмяСервераПакетов, Знач Пул = "") Экспорт +Функция ДляПубликации(Знач АдресХаба, Знач ИмяСервераПакетов, Знач Пул = "", Знач Форсировать = Ложь) Экспорт + + Сервер = СерверПубликации(АдресХаба, ИмяСервераПакетов, Пул); + Если Форсировать Тогда + Сервер.РесурсПубликацииПакетов = Сервер.РесурсПубликацииПакетов + + ?(СтрНайти(Сервер.РесурсПубликацииПакетов, "?") > 0, "&", "?") + "force=1"; + КонецЕсли; + + Возврат Сервер; + +КонецФункции + +// force=1 в ресурсе публикации просит хаб опубликовать поверх занятого номера версии. +Функция СерверПубликации(Знач АдресХаба, Знач ИмяСервераПакетов, Знач Пул) Если ЗначениеЗаполнено(АдресХаба) Или ЗначениеЗаполнено(Пул) Тогда Возврат ИзАдреса(АдресаХаба.Адрес(АдресХаба, Пул)); diff --git a/tests/push-target-test.os b/tests/push-target-test.os index dd9bd4e..a1cc00e 100644 --- a/tests/push-target-test.os +++ b/tests/push-target-test.os @@ -10,6 +10,8 @@ СписокТестов.Добавить("ТестДолжен_ПубликоватьВПулХабаПоУмолчанию"); СписокТестов.Добавить("ТестДолжен_ПубликоватьНаСерверИзНастроекПоИмени"); СписокТестов.Добавить("ТестДолжен_ПубликоватьВХабПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ДобавитьForceКРесурсуПубликации"); + СписокТестов.Добавить("ТестДолжен_НеДобавлятьForceБезОпции"); Возврат СписокТестов; @@ -68,3 +70,23 @@ Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно(КонстантыOpm.РесурсПубликацииПакетов); КонецПроцедуры + +Процедура ТестДолжен_ДобавитьForceКРесурсуПубликации() Экспорт + + Сервер = СерверыПакетов.ДляПубликации("127.0.0.1:3380", "", "corp", Истина); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов, "пул хаба").Равно("/api/v1/pools/corp/push?force=1"); + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("corp", "https://hub.example", , "/push?owner=corp")); + Сервер = СерверыПакетов.ДляПубликации("", "corp", , Истина); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов, "ресурс с параметрами").Равно("/push?owner=corp&force=1"); + +КонецПроцедуры + +Процедура ТестДолжен_НеДобавлятьForceБезОпции() Экспорт + + Ожидаем.Что(СерверыПакетов.ДляПубликации("127.0.0.1:3380", "", "corp").РесурсПубликацииПакетов) + .Равно("/api/v1/pools/corp/push"); + Ожидаем.Что(СерверыПакетов.ДляПубликации("127.0.0.1:3380", "", "corp", Ложь).РесурсПубликацииПакетов) + .Равно("/api/v1/pools/corp/push"); + +КонецПроцедуры From c01df0afec5d7ca82acf6828e79fb045f5f264cc Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 07:19:30 +0400 Subject: [PATCH 14/17] =?UTF-8?q?push=20--oidc:=20=D0=BF=D1=83=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20id-token=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D0=B2=D0=B5=D0=B9=D0=B5=D1=80=D0=B0=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=E2=80=94=20=D0=B2=20GitHub=20Actions=20=D0=BE=D0=BD=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=B0=D1=88=D0=B8=D0=B2=D0=B0=D0=B5=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D1=83=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=BE=D0=B2=20=D1=81=20?= =?UTF-8?q?=D0=B0=D1=83=D0=B4=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=B8=D0=B7=20--oidc-audience=20(=D0=BF=D0=BE=20=D1=83=D0=BC?= =?UTF-8?q?=D0=BE=D0=BB=D1=87=D0=B0=D0=BD=D0=B8=D1=8E=20=D0=B0=D0=B4=D1=80?= =?UTF-8?q?=D0=B5=D1=81=20=D1=85=D0=B0=D0=B1=D0=B0),=20=D0=B2=20GitLab=20C?= =?UTF-8?q?I=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B0=20id=5Ftokens,=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B9=20--oidc-token?= =?UTF-8?q?-var;=20=D0=B1=D0=B5=D0=B7=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5?= =?UTF-8?q?=D0=B9=D0=B5=D1=80=D0=B0=20=E2=80=94=20=D0=BE=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7,=20=D0=BD=D0=B0=D0=B7=D1=8B=D0=B2=D0=B0=D1=8E=D1=89?= =?UTF-8?q?=D0=B8=D0=B9,=20=D1=87=D0=B5=D0=B3=D0=BE=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D1=85=D0=B2=D0=B0=D1=82=D0=B0=D0=B5=D1=82;=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=E2=80=94=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20=D0=A2=D0=BE=D0=BA=D0=B5=D0=BD?= =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D0=B2=D0=B5=D0=B9=D0=B5=D1=80=D0=B0CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 ++++++++ packagedef | 1 + ...20\260\320\275\320\264\320\260Opm_Push.os" | 8 ++ ...20\272\320\265\321\202\320\276\320\262.os" | 2 +- ...\265\320\271\320\265\321\200\320\260CI.os" | 74 ++++++++++++++ ...20\265\320\271\320\265\321\200\320\260.os" | 9 ++ tests/trusted-publish-test.os | 97 +++++++++++++++++++ 7 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 "src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" create mode 100644 "tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" create mode 100644 tests/trusted-publish-test.os diff --git a/README.md b/README.md index 4673205..711da8a 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,47 @@ $ opm install my-package и при расхождении отказывает в установке, не записывая файл. Ответ без заголовка (хаб без его поддержки, файловый сервер) не сверяется. +# Доверенная публикация из CI + +Конвейер GitHub Actions или GitLab CI публикует пакет без токена: `opm push --oidc` берёт у конвейера +OIDC id-token и отправляет его хабу вместо токена, а хаб сверяет его с записью доверия пакета. +Запись доверия (провайдер, репозиторий, при необходимости конвейер и ref) заводится на странице пакета в хабе. + +GitHub Actions выдаёт id-token конвейеру с разрешением `id-token: write`: + +```yaml +permissions: + contents: read + id-token: write +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: otymko/setup-onescript@v1.5.1 + - run: opm install -l && opm build . + - run: opm push --hub https://hub.example.com/corp --oidc my-package-1.0.0.ospx +``` + +GitLab CI кладёт id-token в переменную, названную в блоке `id_tokens`; её имя передаётся опцией `--oidc-token-var`: + +```yaml +publish: + id_tokens: + OPENHUB_TOKEN: + aud: https://hub.example.com + script: + - opm install -l && opm build . + - opm push --hub https://hub.example.com/corp --oidc --oidc-token-var OPENHUB_TOKEN my-package-1.0.0.ospx +``` + +* В GitHub Actions id-token запрашивается с аудиторией, равной адресу хаба (`https://hub.example.com`); + другую аудиторию задаёт `--oidc-audience`. В GitLab CI аудиторию задаёт `aud` в блоке `id_tokens`. +* С `--oidc` токен из `--token` и настройки `ТокенПубликацииПакетов` не отправляется. +* Конвейер не выдал id-token — opm отказывает и называет, чего не хватает: `permissions: id-token: write` + в GitHub Actions, блока `id_tokens` в GitLab CI. +* Подпись id-token и запись доверия проверяет хаб; отказ хаба opm показывает его сообщением. + # Сервера В настоящее время пакеты хранятся на двух серверах: - [hub.oscript.io](http://hub.oscript.io/download) - основной хаб пакетов diff --git a/packagedef b/packagedef index 974e34e..30f3a55 100644 --- a/packagedef +++ b/packagedef @@ -55,5 +55,6 @@ .ОпределяетМодуль("РаботаСПакетами", "src/core/Модули/РаботаСПакетами.os") .ОпределяетМодуль("РежимУстановкиПакетов", "src/core/Модули/РежимУстановкиПакетов.os") .ОпределяетМодуль("СерверыПакетов", "src/core/Модули/СерверыПакетов.os") + .ОпределяетМодуль("ТокенКонвейераCI", "src/core/Модули/ТокенКонвейераCI.os") .ОпределяетМодуль("ОбщегоНазначенияOpm", "src/core/Модули/ОбщегоНазначенияOpm.os") .ИсполняемыйФайл("src/cmd/opm.os"); diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index 0dd767b..6921ba1 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -23,6 +23,9 @@ ; КомандаПриложения.Опция("f file", "", "Маска или имя файла пакета."); КомандаПриложения.Опция("force", Ложь, "Опубликовать поверх уже занятого номера версии."); + КомандаПриложения.Опция("oidc", Ложь, "Публиковать id-token конвейера GitHub Actions или GitLab CI вместо токена."); + КомандаПриложения.Опция("oidc-audience", "", "Аудитория id-token в GitHub Actions. По умолчанию - адрес хаба."); + КомандаПриложения.Опция("oidc-token-var", "", "Переменная среды с id-token из блока id_tokens в GitLab CI."); КомандаПриложения.Опция("c channel", "auto", "Канал публикации.") .ТПеречисление() .Перечисление("auto", "auto", "Автоматическое определение канала. @@ -81,6 +84,11 @@ Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации, ПустаяСтрока(АдресХаба) И ПустаяСтрока(Пул)); Сервер = СерверыПакетов.ДляПубликации(АдресХаба, ИмяСервераПакетов, Пул, Форсировать); + Если КомандаПриложения.ЗначениеОпции("oidc") Тогда + ТокенАвторизации = ТокенКонвейераCI.Получить(Сервер, КомандаПриложения.ЗначениеОпции("oidc-audience"), + КомандаПриложения.ЗначениеОпции("oidc-token-var")); + КонецЕсли; + Лог.Отладка("Публикация на сервер %1", Сервер.ПолучитьИмя()); Лог.Информация(Сервер.ОтправитьПакет(ФайлПакета, Канал, ТокенАвторизации)); diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 154a2c0..e3f6e40 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -200,7 +200,7 @@ КонецФункции // Адрес сервера в сообщениях пользователю: порт указывается, только если он не стандартный. -Функция АдресДляСообщений() +Функция АдресДляСообщений() Экспорт СтандартныйПорт = ?(СтрНачинаетсяС(НРег(Сервер), "https://"), 443, 80); Если Порт = 0 ИЛИ Порт = СтандартныйПорт Тогда diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" new file mode 100644 index 0000000..28891bd --- /dev/null +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" @@ -0,0 +1,74 @@ +#Использовать logos + +// id-token конвейера GitHub Actions или GitLab CI: хаб принимает его вместо токена публикации. + +// Получает id-token конвейера: из переменной среды (GitLab CI) либо у сервиса токенов GitHub Actions. +// +// Параметры: +// Сервер - СерверПакетов - сервер публикации; его адрес - аудитория по умолчанию. +// Аудитория - Строка - аудитория id-token в GitHub Actions; пустая - адрес сервера. +// ИмяПеременной - Строка - переменная среды с id-token из блока id_tokens GitLab CI. +// Соединение - HTTPСоединение - соединение с сервисом токенов GitHub; Неопределено - по его адресу. +// +// Возвращаемое значение: +// Строка - id-token. +// +Функция Получить(Знач Сервер, Знач Аудитория = "", Знач ИмяПеременной = "", Знач Соединение = Неопределено) Экспорт + + Если ЗначениеЗаполнено(ИмяПеременной) Тогда + Токен = ПолучитьПеременнуюСреды(ИмяПеременной); + Если Не ЗначениеЗаполнено(Токен) Тогда + ВызватьИсключение СтрШаблон("Переменная среды %1 с id-token пуста: в .gitlab-ci.yml нужен блок id_tokens с ней", + ИмяПеременной); + КонецЕсли; + Возврат Токен; + КонецЕсли; + + АдресСервиса = ПолучитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_URL"); + ТокенЗапроса = ПолучитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_TOKEN"); + Если Не ЗначениеЗаполнено(АдресСервиса) Или Не ЗначениеЗаполнено(ТокенЗапроса) Тогда + ВызватьИсключение "Конвейер не выдал id-token: в GitHub Actions нужно permissions: id-token: write," + + " в GitLab CI - блок id_tokens и имя его переменной в опции <--oidc-token-var>"; + КонецЕсли; + + Если Не ЗначениеЗаполнено(Аудитория) Тогда + Аудитория = Сервер.АдресДляСообщений(); + КонецЕсли; + + КонецХоста = СтрНайти(АдресСервиса, "/", , СтрНайти(АдресСервиса, "://") + 3); + Хост = Лев(АдресСервиса, КонецХоста - 1); + Ресурс = Сред(АдресСервиса, КонецХоста); + Ресурс = Ресурс + ?(СтрНайти(Ресурс, "?") > 0, "&", "?") + + "audience=" + КодироватьСтроку(Аудитория, СпособКодированияСтроки.КодировкаURL); + + Запрос = Новый HTTPЗапрос(Ресурс); + Запрос.Заголовки.Вставить("Authorization", "bearer " + ТокенЗапроса); + + Если Соединение = Неопределено Тогда + Соединение = Новый HTTPСоединение(Хост); + КонецЕсли; + + Попытка + Ответ = Соединение.Получить(Запрос); + Исключение + Логирование.ПолучитьЛог("oscript.app.opm").Отладка("Запрос id-token к %1 не удался: %2", Хост, ОписаниеОшибки()); + ВызватьИсключение СтрШаблон("Не удалось подключиться к %1", Хост); + КонецПопытки; + + Тело = Ответ.ПолучитьТелоКакСтроку(); + Если Ответ.КодСостояния <> 200 Тогда + ВызватьИсключение СтрШаблон("Сервис id-token GitHub ответил кодом %1: %2", Ответ.КодСостояния, Тело); + КонецЕсли; + + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(Тело); + Токен = ПрочитатьJSON(Чтение, Истина)["value"]; + Чтение.Закрыть(); + + Если Не ЗначениеЗаполнено(Токен) Тогда + ВызватьИсключение "В ответе сервиса id-token GitHub нет поля value"; + КонецЕсли; + + Возврат Токен; + +КонецФункции diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" new file mode 100644 index 0000000..99b4d34 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" @@ -0,0 +1,9 @@ +// Соединение с сервисом токенов GitHub: запоминает запрос и отдаёт заданный ответ. + +Перем Запрос Экспорт; +Перем Ответ Экспорт; + +Функция Получить(Знач ВходящийЗапрос) Экспорт + Запрос = ВходящийЗапрос; + Возврат Ответ; +КонецФункции diff --git a/tests/trusted-publish-test.os b/tests/trusted-publish-test.os new file mode 100644 index 0000000..99a6bd1 --- /dev/null +++ b/tests/trusted-publish-test.os @@ -0,0 +1,97 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ВзятьТокенИзПеременнойGitLab"); + СписокТестов.Добавить("ТестДолжен_ОтказатьПриПустойПеременнойGitLab"); + СписокТестов.Добавить("ТестДолжен_ЗапроситьТокенУСервисаGitHub"); + СписокТестов.Добавить("ТестДолжен_ЗапроситьТокенСАдресомХабаВАудитории"); + СписокТестов.Добавить("ТестДолжен_ОтказатьБезПеременныхКонвейера"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПослеЗапускаТеста() Экспорт + УстановитьПеременнуюСреды("OPM_TEST_ID_TOKEN", ""); + УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_URL", ""); + УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_TOKEN", ""); +КонецПроцедуры + +Процедура ТестДолжен_ВзятьТокенИзПеременнойGitLab() Экспорт + + УстановитьПеременнуюСреды("OPM_TEST_ID_TOKEN", "id.token.gitlab"); + + Ожидаем.Что(ТокенКонвейераCI.Получить(СерверХаба(), "", "OPM_TEST_ID_TOKEN")).Равно("id.token.gitlab"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьПриПустойПеременнойGitLab() Экспорт + + Параметры = Новый Массив; + Параметры.Добавить(СерверХаба()); + Параметры.Добавить(""); + Параметры.Добавить("OPM_TEST_ID_TOKEN"); + + Ожидаем.Что(ТокенКонвейераCI).Метод("Получить", Параметры).ВыбрасываетИсключение("блок id_tokens"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапроситьТокенУСервисаGitHub() Экспорт + + Соединение = СоединениеGitHub(); + + Ожидаем.Что(ТокенКонвейераCI.Получить(СерверХаба(), "https://aud.example", "", Соединение)).Равно("id.token.github"); + Ожидаем.Что(Соединение.Запрос.АдресРесурса).Равно("/idtoken?api-version=2.0&audience=https%3A%2F%2Faud.example"); + Ожидаем.Что(Соединение.Запрос.Заголовки["Authorization"]).Равно("bearer токен-запроса"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапроситьТокенСАдресомХабаВАудитории() Экспорт + + Соединение = СоединениеGitHub(); + + ТокенКонвейераCI.Получить(СерверХаба(), "", "", Соединение); + + Ожидаем.Что(Соединение.Запрос.АдресРесурса).Равно("/idtoken?api-version=2.0&audience=https%3A%2F%2Fhub.example"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьБезПеременныхКонвейера() Экспорт + + Параметры = Новый Массив; + Параметры.Добавить(СерверХаба()); + + Ожидаем.Что(ТокенКонвейераCI).Метод("Получить", Параметры).ВыбрасываетИсключение("permissions: id-token: write"); + +КонецПроцедуры + +Функция СерверХаба() + Возврат СерверыПакетов.ИзАдреса(АдресаХаба.АдресПула("hub.example/corp")); +КонецФункции + +Функция СоединениеGitHub() + + УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_URL", "https://tokens.example/idtoken?api-version=2.0"); + УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "токен-запроса"); + + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Ответ.Тело = "{""count"": 1, ""value"": ""id.token.github""}"; + + Соединение = ЗагрузитьЗаглушку("ЗаглушкаКонвейера.os"); + Соединение.Ответ = Ответ; + + Возврат Соединение; + +КонецФункции + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции From d5fff2634a6e5bda5a5e298c0c5b7146b99d8920 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 07:51:34 +0400 Subject: [PATCH 15/17] =?UTF-8?q?push:=20=D0=BE=D0=BF=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?--oidc,=20--oidc-audience,=20--oidc-token-var=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=B2=20--trusted,=20--trusted-audience,=20--trusted-?= =?UTF-8?q?token-var?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++++------ ...0\274\320\260\320\275\320\264\320\260Opm_Push.os" | 12 ++++++------ ...262\320\265\320\271\320\265\321\200\320\260CI.os" | 2 +- tests/trusted-publish-test.os | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 711da8a..571a5b5 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ $ opm install my-package # Доверенная публикация из CI -Конвейер GitHub Actions или GitLab CI публикует пакет без токена: `opm push --oidc` берёт у конвейера +Конвейер GitHub Actions или GitLab CI публикует пакет без токена: `opm push --trusted` берёт у конвейера OIDC id-token и отправляет его хабу вместо токена, а хаб сверяет его с записью доверия пакета. Запись доверия (провайдер, репозиторий, при необходимости конвейер и ref) заводится на странице пакета в хабе. @@ -108,10 +108,10 @@ jobs: - uses: actions/checkout@v4 - uses: otymko/setup-onescript@v1.5.1 - run: opm install -l && opm build . - - run: opm push --hub https://hub.example.com/corp --oidc my-package-1.0.0.ospx + - run: opm push --hub https://hub.example.com/corp --trusted my-package-1.0.0.ospx ``` -GitLab CI кладёт id-token в переменную, названную в блоке `id_tokens`; её имя передаётся опцией `--oidc-token-var`: +GitLab CI кладёт id-token в переменную, названную в блоке `id_tokens`; её имя передаётся опцией `--trusted-token-var`: ```yaml publish: @@ -120,12 +120,12 @@ publish: aud: https://hub.example.com script: - opm install -l && opm build . - - opm push --hub https://hub.example.com/corp --oidc --oidc-token-var OPENHUB_TOKEN my-package-1.0.0.ospx + - opm push --hub https://hub.example.com/corp --trusted --trusted-token-var OPENHUB_TOKEN my-package-1.0.0.ospx ``` * В GitHub Actions id-token запрашивается с аудиторией, равной адресу хаба (`https://hub.example.com`); - другую аудиторию задаёт `--oidc-audience`. В GitLab CI аудиторию задаёт `aud` в блоке `id_tokens`. -* С `--oidc` токен из `--token` и настройки `ТокенПубликацииПакетов` не отправляется. + другую аудиторию задаёт `--trusted-audience`. В GitLab CI аудиторию задаёт `aud` в блоке `id_tokens`. +* С `--trusted` токен из `--token` и настройки `ТокенПубликацииПакетов` не отправляется. * Конвейер не выдал id-token — opm отказывает и называет, чего не хватает: `permissions: id-token: write` в GitHub Actions, блока `id_tokens` в GitLab CI. * Подпись id-token и запись доверия проверяет хаб; отказ хаба opm показывает его сообщением. diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index 6921ba1..393d56a 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -23,9 +23,9 @@ ; КомандаПриложения.Опция("f file", "", "Маска или имя файла пакета."); КомандаПриложения.Опция("force", Ложь, "Опубликовать поверх уже занятого номера версии."); - КомандаПриложения.Опция("oidc", Ложь, "Публиковать id-token конвейера GitHub Actions или GitLab CI вместо токена."); - КомандаПриложения.Опция("oidc-audience", "", "Аудитория id-token в GitHub Actions. По умолчанию - адрес хаба."); - КомандаПриложения.Опция("oidc-token-var", "", "Переменная среды с id-token из блока id_tokens в GitLab CI."); + КомандаПриложения.Опция("trusted", Ложь, "Публиковать id-token конвейера GitHub Actions или GitLab CI вместо токена."); + КомандаПриложения.Опция("trusted-audience", "", "Аудитория id-token в GitHub Actions. По умолчанию - адрес хаба."); + КомандаПриложения.Опция("trusted-token-var", "", "Переменная среды с id-token из блока id_tokens в GitLab CI."); КомандаПриложения.Опция("c channel", "auto", "Канал публикации.") .ТПеречисление() .Перечисление("auto", "auto", "Автоматическое определение канала. @@ -84,9 +84,9 @@ Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации, ПустаяСтрока(АдресХаба) И ПустаяСтрока(Пул)); Сервер = СерверыПакетов.ДляПубликации(АдресХаба, ИмяСервераПакетов, Пул, Форсировать); - Если КомандаПриложения.ЗначениеОпции("oidc") Тогда - ТокенАвторизации = ТокенКонвейераCI.Получить(Сервер, КомандаПриложения.ЗначениеОпции("oidc-audience"), - КомандаПриложения.ЗначениеОпции("oidc-token-var")); + Если КомандаПриложения.ЗначениеОпции("trusted") Тогда + ТокенАвторизации = ТокенКонвейераCI.Получить(Сервер, КомандаПриложения.ЗначениеОпции("trusted-audience"), + КомандаПриложения.ЗначениеОпции("trusted-token-var")); КонецЕсли; Лог.Отладка("Публикация на сервер %1", Сервер.ПолучитьИмя()); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" index 28891bd..93b4a6f 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" @@ -28,7 +28,7 @@ ТокенЗапроса = ПолучитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_TOKEN"); Если Не ЗначениеЗаполнено(АдресСервиса) Или Не ЗначениеЗаполнено(ТокенЗапроса) Тогда ВызватьИсключение "Конвейер не выдал id-token: в GitHub Actions нужно permissions: id-token: write," - + " в GitLab CI - блок id_tokens и имя его переменной в опции <--oidc-token-var>"; + + " в GitLab CI - блок id_tokens и имя его переменной в опции <--trusted-token-var>"; КонецЕсли; Если Не ЗначениеЗаполнено(Аудитория) Тогда diff --git a/tests/trusted-publish-test.os b/tests/trusted-publish-test.os index 99a6bd1..87d3e5f 100644 --- a/tests/trusted-publish-test.os +++ b/tests/trusted-publish-test.os @@ -74,7 +74,7 @@ КонецПроцедуры Функция СерверХаба() - Возврат СерверыПакетов.ИзАдреса(АдресаХаба.АдресПула("hub.example/corp")); + Возврат СерверыПакетов.ИзАдреса(АдресаХаба.Адрес("hub.example", "corp")); КонецФункции Функция СоединениеGitHub() From 9f57e408b52542f5a37f2d0ae8c080f52e757e5e Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 11:23:43 +0400 Subject: [PATCH 16/17] =?UTF-8?q?push=20--trusted:=20=D0=BE=D0=BF=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20--trusted-token-var=20=D1=81=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B0=20=E2=80=94=20id-token=20GitLab=20CI=20=D1=87=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=B8=D0=B7=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D0=BE=D0=B9=20OPENHU?= =?UTF-8?q?B=5FTOKEN,=20=D0=BF=D0=BE=D0=B4=20=D1=8D=D1=82=D0=B8=D0=BC=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5=D0=BC=20=D0=B5=D1=91=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB=D1=8F=D1=8E=D1=82=20=D0=B2=20?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D0=BA=D0=B5=20id=5Ftokens;=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=D0=B7=20=D0=B1=D0=B5=D0=B7=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D0=B2=D0=B5=D0=B9=D0=B5=D1=80=D0=B0=20=D0=BD=D0=B0=D0=B7=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D1=82=20=D1=8D=D1=82=D0=BE=20=D0=B8=D0=BC?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- ...320\260\320\275\320\264\320\260Opm_Push.os" | 4 +--- ...0\265\320\271\320\265\321\200\320\260CI.os" | 15 +++++---------- tests/trusted-publish-test.os | 18 ++++++++---------- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 571a5b5..70d7623 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ jobs: - run: opm push --hub https://hub.example.com/corp --trusted my-package-1.0.0.ospx ``` -GitLab CI кладёт id-token в переменную, названную в блоке `id_tokens`; её имя передаётся опцией `--trusted-token-var`: +GitLab CI кладёт id-token в переменную блока `id_tokens`; opm читает её под именем `OPENHUB_TOKEN`: ```yaml publish: @@ -120,14 +120,14 @@ publish: aud: https://hub.example.com script: - opm install -l && opm build . - - opm push --hub https://hub.example.com/corp --trusted --trusted-token-var OPENHUB_TOKEN my-package-1.0.0.ospx + - opm push --hub https://hub.example.com/corp --trusted my-package-1.0.0.ospx ``` * В GitHub Actions id-token запрашивается с аудиторией, равной адресу хаба (`https://hub.example.com`); другую аудиторию задаёт `--trusted-audience`. В GitLab CI аудиторию задаёт `aud` в блоке `id_tokens`. * С `--trusted` токен из `--token` и настройки `ТокенПубликацииПакетов` не отправляется. * Конвейер не выдал id-token — opm отказывает и называет, чего не хватает: `permissions: id-token: write` - в GitHub Actions, блока `id_tokens` в GitLab CI. + в GitHub Actions, переменной `OPENHUB_TOKEN` в блоке `id_tokens` в GitLab CI. * Подпись id-token и запись доверия проверяет хаб; отказ хаба opm показывает его сообщением. # Сервера diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index 393d56a..5c9a4e1 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -25,7 +25,6 @@ КомандаПриложения.Опция("force", Ложь, "Опубликовать поверх уже занятого номера версии."); КомандаПриложения.Опция("trusted", Ложь, "Публиковать id-token конвейера GitHub Actions или GitLab CI вместо токена."); КомандаПриложения.Опция("trusted-audience", "", "Аудитория id-token в GitHub Actions. По умолчанию - адрес хаба."); - КомандаПриложения.Опция("trusted-token-var", "", "Переменная среды с id-token из блока id_tokens в GitLab CI."); КомандаПриложения.Опция("c channel", "auto", "Канал публикации.") .ТПеречисление() .Перечисление("auto", "auto", "Автоматическое определение канала. @@ -85,8 +84,7 @@ Сервер = СерверыПакетов.ДляПубликации(АдресХаба, ИмяСервераПакетов, Пул, Форсировать); Если КомандаПриложения.ЗначениеОпции("trusted") Тогда - ТокенАвторизации = ТокенКонвейераCI.Получить(Сервер, КомандаПриложения.ЗначениеОпции("trusted-audience"), - КомандаПриложения.ЗначениеОпции("trusted-token-var")); + ТокенАвторизации = ТокенКонвейераCI.Получить(Сервер, КомандаПриложения.ЗначениеОпции("trusted-audience")); КонецЕсли; Лог.Отладка("Публикация на сервер %1", Сервер.ПолучитьИмя()); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" index 93b4a6f..5523af9 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" @@ -2,25 +2,20 @@ // id-token конвейера GitHub Actions или GitLab CI: хаб принимает его вместо токена публикации. -// Получает id-token конвейера: из переменной среды (GitLab CI) либо у сервиса токенов GitHub Actions. +// Получает id-token конвейера: из переменной OPENHUB_TOKEN (GitLab CI) либо у сервиса токенов GitHub Actions. // // Параметры: // Сервер - СерверПакетов - сервер публикации; его адрес - аудитория по умолчанию. // Аудитория - Строка - аудитория id-token в GitHub Actions; пустая - адрес сервера. -// ИмяПеременной - Строка - переменная среды с id-token из блока id_tokens GitLab CI. // Соединение - HTTPСоединение - соединение с сервисом токенов GitHub; Неопределено - по его адресу. // // Возвращаемое значение: // Строка - id-token. // -Функция Получить(Знач Сервер, Знач Аудитория = "", Знач ИмяПеременной = "", Знач Соединение = Неопределено) Экспорт +Функция Получить(Знач Сервер, Знач Аудитория = "", Знач Соединение = Неопределено) Экспорт - Если ЗначениеЗаполнено(ИмяПеременной) Тогда - Токен = ПолучитьПеременнуюСреды(ИмяПеременной); - Если Не ЗначениеЗаполнено(Токен) Тогда - ВызватьИсключение СтрШаблон("Переменная среды %1 с id-token пуста: в .gitlab-ci.yml нужен блок id_tokens с ней", - ИмяПеременной); - КонецЕсли; + Токен = ПолучитьПеременнуюСреды("OPENHUB_TOKEN"); + Если ЗначениеЗаполнено(Токен) Тогда Возврат Токен; КонецЕсли; @@ -28,7 +23,7 @@ ТокенЗапроса = ПолучитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_TOKEN"); Если Не ЗначениеЗаполнено(АдресСервиса) Или Не ЗначениеЗаполнено(ТокенЗапроса) Тогда ВызватьИсключение "Конвейер не выдал id-token: в GitHub Actions нужно permissions: id-token: write," - + " в GitLab CI - блок id_tokens и имя его переменной в опции <--trusted-token-var>"; + + " в GitLab CI - переменная OPENHUB_TOKEN в блоке id_tokens"; КонецЕсли; Если Не ЗначениеЗаполнено(Аудитория) Тогда diff --git a/tests/trusted-publish-test.os b/tests/trusted-publish-test.os index 87d3e5f..62dbd3f 100644 --- a/tests/trusted-publish-test.os +++ b/tests/trusted-publish-test.os @@ -10,7 +10,7 @@ СписокТестов = Новый Массив; СписокТестов.Добавить("ТестДолжен_ВзятьТокенИзПеременнойGitLab"); - СписокТестов.Добавить("ТестДолжен_ОтказатьПриПустойПеременнойGitLab"); + СписокТестов.Добавить("ТестДолжен_НазватьПеременнуюGitLabВОтказе"); СписокТестов.Добавить("ТестДолжен_ЗапроситьТокенУСервисаGitHub"); СписокТестов.Добавить("ТестДолжен_ЗапроситьТокенСАдресомХабаВАудитории"); СписокТестов.Добавить("ТестДолжен_ОтказатьБезПеременныхКонвейера"); @@ -20,27 +20,25 @@ КонецФункции Процедура ПослеЗапускаТеста() Экспорт - УстановитьПеременнуюСреды("OPM_TEST_ID_TOKEN", ""); + УстановитьПеременнуюСреды("OPENHUB_TOKEN", ""); УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_URL", ""); УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_TOKEN", ""); КонецПроцедуры Процедура ТестДолжен_ВзятьТокенИзПеременнойGitLab() Экспорт - УстановитьПеременнуюСреды("OPM_TEST_ID_TOKEN", "id.token.gitlab"); + УстановитьПеременнуюСреды("OPENHUB_TOKEN", "id.token.gitlab"); - Ожидаем.Что(ТокенКонвейераCI.Получить(СерверХаба(), "", "OPM_TEST_ID_TOKEN")).Равно("id.token.gitlab"); + Ожидаем.Что(ТокенКонвейераCI.Получить(СерверХаба())).Равно("id.token.gitlab"); КонецПроцедуры -Процедура ТестДолжен_ОтказатьПриПустойПеременнойGitLab() Экспорт +Процедура ТестДолжен_НазватьПеременнуюGitLabВОтказе() Экспорт Параметры = Новый Массив; Параметры.Добавить(СерверХаба()); - Параметры.Добавить(""); - Параметры.Добавить("OPM_TEST_ID_TOKEN"); - Ожидаем.Что(ТокенКонвейераCI).Метод("Получить", Параметры).ВыбрасываетИсключение("блок id_tokens"); + Ожидаем.Что(ТокенКонвейераCI).Метод("Получить", Параметры).ВыбрасываетИсключение("OPENHUB_TOKEN в блоке id_tokens"); КонецПроцедуры @@ -48,7 +46,7 @@ Соединение = СоединениеGitHub(); - Ожидаем.Что(ТокенКонвейераCI.Получить(СерверХаба(), "https://aud.example", "", Соединение)).Равно("id.token.github"); + Ожидаем.Что(ТокенКонвейераCI.Получить(СерверХаба(), "https://aud.example", Соединение)).Равно("id.token.github"); Ожидаем.Что(Соединение.Запрос.АдресРесурса).Равно("/idtoken?api-version=2.0&audience=https%3A%2F%2Faud.example"); Ожидаем.Что(Соединение.Запрос.Заголовки["Authorization"]).Равно("bearer токен-запроса"); @@ -58,7 +56,7 @@ Соединение = СоединениеGitHub(); - ТокенКонвейераCI.Получить(СерверХаба(), "", "", Соединение); + ТокенКонвейераCI.Получить(СерверХаба(), "", Соединение); Ожидаем.Что(Соединение.Запрос.АдресРесурса).Равно("/idtoken?api-version=2.0&audience=https%3A%2F%2Fhub.example"); From 56442a417a467ff021bc9e0619c15849f45b2752 Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Fri, 16 Oct 2026 11:50:48 +0400 Subject: [PATCH 17/17] =?UTF-8?q?install=20--all-pools:=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D1=8E=20=D0=B2=D1=8B=D0=B1=D0=B8=D1=80=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=20=D1=85=D0=B0=D0=B1=20=D0=BF=D0=BE=20=D0=B2=D1=81?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=BD=D1=8B?= =?UTF-8?q?=D0=BC=20=D0=BF=D1=83=D0=BB=D0=B0=D0=BC=20=E2=80=94=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B5=D1=80=20=D1=85=D0=B0=D0=B1=D0=B0=20?= =?UTF-8?q?=D1=81=20=D0=B2=D1=8B=D0=B4=D0=B0=D1=87=D0=B5=D0=B9=20/api/v1/d?= =?UTF-8?q?ownload/<=D0=BF=D0=B0=D0=BA=D0=B5=D1=82>/<=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB>=20=D1=82=D0=B5=D0=BC=D0=B8=20=D0=B6=D0=B5=20spec=20?= =?UTF-8?q?=D0=B8=20engine=20(=D0=A1=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D1=8B?= =?UTF-8?q?=D0=9F=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2.=D0=98=D0=B7=D0=90?= =?UTF-8?q?=D0=B4=D1=80=D0=B5=D1=81=D0=B0=20=D0=B8=20=D0=A0=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=A1=D0=9F=D0=B0=D0=BA=D0=B5=D1=82=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8.=D0=9F=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=D0=A1=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=A5=D0=B0?= =?UTF-8?q?=D0=B1=D0=B0=20=D1=81=20=D0=BF=D1=80=D0=B8=D0=B7=D0=BD=D0=B0?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=20=D0=9F=D0=BE=D0=92=D1=81=D0=B5=D0=BC=D0=9F?= =?UTF-8?q?=D1=83=D0=BB=D0=B0=D0=BC,=20=D0=B8=D0=BC=D1=8F=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B5=D1=80=D0=B0=20=D1=85=D0=BE=D1=81=D1=82/*);?= =?UTF-8?q?=20=D1=81=20--pool=20=D0=BD=D0=B5=20=D1=81=D0=BE=D1=87=D0=B5?= =?UTF-8?q?=D1=82=D0=B0=D0=B5=D1=82=D1=81=D1=8F;=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D1=8B=20hub-address,=20=D1=84=D0=B8=D1=87=D0=B0=20instal?= =?UTF-8?q?l-hub,=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++ features/install-hub.feature | 12 +++++++ ...260\320\275\320\264\320\260Opm_Install.os" | 17 +++++++--- ...20\265\321\202\320\260\320\274\320\270.os" | 5 +-- ...20\272\320\265\321\202\320\276\320\262.os" | 8 +++-- tests/hub-address-test.os | 33 +++++++++++++++++++ 6 files changed, 69 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 70d7623..a471927 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ opm install localhost:3380/corp/my-package@stable # хост, порт, пул opm install hub.example.com/asserts # пакет в пуле хаба по умолчанию opm install https://hub.example.com/public/asserts # схема указана явно opm install --hub localhost:3380 --pool corp my-package@stable +opm install --hub localhost:3380 --all-pools my-package@stable # версию выбирает хаб по всем доступным пулам opm push --hub localhost:3380 --pool corp my-package-1.0.0.ospx opm push --hub https://hub.example.com --token <токен> my-package-1.0.0.ospx ``` @@ -38,6 +39,8 @@ opm push --hub https://hub.example.com --token <токен> my-package-1.0.0.osp * В адресе пакета последний сегмент пути — пакет, сегмент перед ним — пул; без пула — пул хаба по умолчанию. Зависимости пакета ставятся с того же сервера, у `install --hub` — с хаба из опций. +* `install --all-pools` просит хаб выбрать версию по всем пулам, открытым токену (без токена — по публичным): + адреса `/api/v1/download/<пакет>/<файл>` с теми же `spec` и `engine`; с `--pool` не сочетается. * Отказ: путь в `--hub`, адрес пакета вместе с `--hub`, `install --hub` вместе с `-a`, `-f`, `-u`, `-m`. * Строка считается адресом, если в ней есть схема либо путь после хоста с портом, с точкой в имени или `localhost`. Остальное — имя пакета. diff --git a/features/install-hub.feature b/features/install-hub.feature index 6fa3d0a..a08bf8f 100644 --- a/features/install-hub.feature +++ b/features/install-hub.feature @@ -18,3 +18,15 @@ Когда Я выполняю команду "opm" Тогда Вывод команды "opm" содержит "задан адресом вместе с опцией <--hub>: укажите одно из двух" И Код возврата команды "opm" равен 1 + +Сценарий: Все пулы вместе с одним пулом + Допустим Я добавляю параметр "install" для команды "opm" + И Я добавляю параметр "--hub" для команды "opm" + И Я добавляю параметр "hub.example" для команды "opm" + И Я добавляю параметр "--pool" для команды "opm" + И Я добавляю параметр "corp" для команды "opm" + И Я добавляю параметр "--all-pools" для команды "opm" + И Я добавляю параметр "pkg" для команды "opm" + Когда Я выполняю команду "opm" + Тогда Вывод команды "opm" содержит "Опция <--all-pools> не сочетается с опцией <--pool>" + И Код возврата команды "opm" равен 1 diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" index f8ef647..3672688 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" @@ -27,6 +27,7 @@ .ВОкружении("OPM_HUB"); КомандаПриложения.Опция("pool", "", "Пул хаба; без пула - пул хаба по умолчанию.") .ВОкружении("OPM_POOL"); + КомандаПриложения.Опция("all-pools", Ложь, "Ставить из всех доступных пулов хаба: версию выбирает хаб среди пулов, открытых токену."); КомандаПриложения.Аргумент("PACKAGE", "", ПараметрыПриложенияOpm.ОписаниеАргументаПакет()) .ТМассивСтрок() @@ -51,6 +52,7 @@ ИмяСервера = КомандаПриложения.ЗначениеОпции("mirror"); Хаб = КомандаПриложения.ЗначениеОпции("hub"); Пул = КомандаПриложения.ЗначениеОпции("pool"); + ПоВсемПулам = КомандаПриложения.ЗначениеОпции("all-pools"); РежимУстановки = РежимУстановкиПакетов.Глобально; Если УстановкаВЛокальныйКаталог Тогда @@ -85,12 +87,12 @@ НастройкаУстановки.СоздаватьФайлыЗапуска = СоздаватьФайлыЗапуска; НастройкаУстановки.ИмяСервера = ИмяСервера; - Если ЗначениеЗаполнено(Хаб) Или ЗначениеЗаполнено(Пул) Тогда + Если ЗначениеЗаполнено(Хаб) Или ЗначениеЗаполнено(Пул) Или ПоВсемПулам Тогда Если УстановкаВсехПакетов Или ЗначениеЗаполнено(ФайлПакетаУстановки) Или ЗначениеЗаполнено(ИнтернетСсылкаНаПакет) Или ЗначениеЗаполнено(ИмяСервера) Тогда ВызватьИсключение "Опция <--hub> не сочетается с опциями <-a>, <-f>, <-u> и <-m>"; КонецЕсли; - НастройкаУстановки.ИмяСервера = ПодключитьХаб(Хаб, Пул, МассивПакетовКУстановке); + НастройкаУстановки.ИмяСервера = ПодключитьХаб(Хаб, Пул, ПоВсемПулам, МассивПакетовКУстановке); КонецЕсли; Если УстановкаВсехПакетов Тогда @@ -159,17 +161,22 @@ КонецПроцедуры -// Подключает пул хаба из опций --hub и --pool: пакеты и их зависимости ставятся с него. +// Подключает хаб из опций --hub, --pool и --all-pools: пакеты и их зависимости ставятся с него. // // Параметры: // Хаб - Строка - [схема://]хост[:порт]. // Пул - Строка - пул хаба; пустой - пул хаба по умолчанию. +// ПоВсемПулам - Булево - выдача по всем доступным пулам хаба; с пулом не сочетается. // ИменаПакетов - Массив - имена пакетов из аргумента PACKAGE. // // Возвращаемое значение: // Строка - имя сервера пула в настройках. // -Функция ПодключитьХаб(Знач Хаб, Знач Пул, Знач ИменаПакетов) +Функция ПодключитьХаб(Знач Хаб, Знач Пул, Знач ПоВсемПулам, Знач ИменаПакетов) + + Если ПоВсемПулам И ЗначениеЗаполнено(Пул) Тогда + ВызватьИсключение "Опция <--all-pools> не сочетается с опцией <--pool>: укажите одно из двух"; + КонецЕсли; Адрес = АдресаХаба.Адрес(Хаб, Пул); Для Каждого ИмяПакета Из ИменаПакетов Цикл @@ -178,7 +185,7 @@ КонецЕсли; КонецЦикла; - Возврат РаботаСПакетами.ПодключитьСерверХаба(Адрес); + Возврат РаботаСПакетами.ПодключитьСерверХаба(Адрес, ПоВсемПулам); КонецФункции diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" index 1126259..afb5dcb 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" @@ -91,13 +91,14 @@ // // Параметры: // Адрес - Структура - результат АдресаХаба.АдресПакета или АдресаХаба.Адрес. +// ПоВсемПулам - Булево - выдача по всем доступным пулам хаба вместо пула адреса. // // Возвращаемое значение: // Строка - имя сервера в настройках. // -Функция ПодключитьСерверХаба(Знач Адрес) Экспорт +Функция ПодключитьСерверХаба(Знач Адрес, Знач ПоВсемПулам = Ложь) Экспорт - Сервер = СерверыПакетов.ИзАдреса(Адрес); + Сервер = СерверыПакетов.ИзАдреса(Адрес, ПоВсемПулам); НастройкиOpm.ДобавитьСерверПакетов(Сервер); ВнутреннийМенеджерПолученияПакетов = Неопределено; diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index b2769e3..b5a5a91 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -32,16 +32,20 @@ // // Параметры: // Адрес - Структура - результат АдресаХаба.АдресПакета или АдресаХаба.Адрес. +// ПоВсемПулам - Булево - выдача по всем доступным пулам хаба вместо пула адреса. // // Возвращаемое значение: // СерверПакетов - сервер выдачи и публикации пула. // -Функция ИзАдреса(Знач Адрес) Экспорт +Функция ИзАдреса(Знач Адрес, Знач ПоВсемПулам = Ложь) Экспорт Имя = Адрес.Сервер + ?(Адрес.Порт = 0, "", ":" + Формат(Адрес.Порт, "ЧГ=0")); ПутьВыдачи = "/download/"; ПутьПубликации = "/push"; - Если ЗначениеЗаполнено(Адрес.Пул) Тогда + Если ПоВсемПулам Тогда + Имя = Имя + "/*"; + ПутьВыдачи = "/api/v1/download/"; + ИначеЕсли ЗначениеЗаполнено(Адрес.Пул) Тогда Имя = Имя + "/" + Адрес.Пул; ПутьВыдачи = СтрШаблон("/api/v1/pools/%1/download/", Адрес.Пул); ПутьПубликации = СтрШаблон("/api/v1/pools/%1/push", Адрес.Пул); diff --git a/tests/hub-address-test.os b/tests/hub-address-test.os index b8e916b..854d284 100644 --- a/tests/hub-address-test.os +++ b/tests/hub-address-test.os @@ -15,9 +15,11 @@ СписокТестов.Добавить("ТестДолжен_ВыбратьСхемуПоУмолчанию"); СписокТестов.Добавить("ТестДолжен_ОтказатьНаНеразобранномАдресе"); СписокТестов.Добавить("ТестДолжен_СобратьСерверПулаПоАдресу"); + СписокТестов.Добавить("ТестДолжен_СобратьСерверВсехПуловПоАдресу"); СписокТестов.Добавить("ТестДолжен_ВзятьАвторизациюАдресаИзНастроек"); СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетССервераИзАдреса"); СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетСХабаИзОпций"); + СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетСоВсехПуловХаба"); Возврат СписокТестов; @@ -108,6 +110,16 @@ КонецПроцедуры +Процедура ТестДолжен_СобратьСерверВсехПуловПоАдресу() Экспорт + + Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.Адрес("hub.example:3380"), Истина); + + Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("http://hub.example:3380/*"); + Ожидаем.Что(Сервер.ПутьНаСервере).Равно("/api/v1/download/"); + Ожидаем.Что(Сервер.АдресРесурса("pkg/pkg-stable.ospx", "stable")).Содержит("/api/v1/download/pkg/pkg-stable.ospx?spec=stable&engine="); + +КонецПроцедуры + Процедура ТестДолжен_ВзятьАвторизациюАдресаИзНастроек() Экспорт НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("чужой", "https://other.example", , , 0, 0, "Bearer чужой")); @@ -161,6 +173,27 @@ КонецПроцедуры +Процедура ТестДолжен_УстанавливатьПакетСоВсехПуловХаба() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, + "fixtures", "ЗаглушкаСервераПакетов.os"))); + + НастройкаУстановки = РаботаСПакетами.ПолучитьНастройкуУстановки(); + НастройкаУстановки.ИмяСервера = РаботаСПакетами.ПодключитьСерверХаба(АдресаХаба.Адрес("127.0.0.1:1"), Истина); + + Ожидаем.Что(НастройкаУстановки.ИмяСервера).Равно("http://127.0.0.1:1/*"); + Ожидаем.Что(НастройкиOpm.ПолучитьНастройки().СервераПакетов[1].ПутьНаСервере).Равно("/api/v1/download/"); + + Попытка + РаботаСПакетами.УстановитьПакетИзОблака("pkg@1.0.0", РежимУстановкиПакетов.Локально, + ОбъединитьПути(юТест.ИмяВременногоФайла(), "oscript_modules"), НастройкаУстановки); + ВызватьИсключение "установка без хаба должна отказать"; + Исключение + Ожидаем.Что(ОписаниеОшибки()).Содержит("Ошибка установки пакета pkg"); + КонецПопытки; + +КонецПроцедуры + Функция ЗначенияПараметров(Знач Значение) Параметры = Новый Массив; Параметры.Добавить(Значение);