diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index e7479741..1023c367 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -1,27 +1,27 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "csharpier": { - "version": "1.3.0", - "commands": [ - "csharpier" - ], - "rollForward": false - }, - "husky": { - "version": "0.9.1", - "commands": [ - "husky" - ], - "rollForward": false - }, - "dotnet-outdated-tool": { - "version": "4.8.1", - "commands": [ - "dotnet-outdated" - ], - "rollForward": false - } - } +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.3.0", + "commands": [ + "csharpier" + ], + "rollForward": false + }, + "husky": { + "version": "0.9.1", + "commands": [ + "husky" + ], + "rollForward": false + }, + "dotnet-outdated-tool": { + "version": "4.8.1", + "commands": [ + "dotnet-outdated" + ], + "rollForward": false + } + } } \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index a95d5c6f..cc6a576d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,27 +1,27 @@ -**/.mount -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md +**/.mount +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md auth.conf \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 7490f8db..b1fe4d1d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,222 +1,208 @@ -# https://editorconfig.org - -# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names -# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions -# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview - -# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md -# https://github.com/dotnet/runtime/blob/main/.editorconfig - -# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format -# dotnet format style --verify-no-changes --severity=info --verbosity=detailed - -# Root config -root = true - -# Defaults -[*] -charset = utf-8 -indent_size = 4 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -# Markdown files -[*.md] -end_of_line = crlf -trim_trailing_whitespace = false - -# Xml files -[*.{xml,csproj,props,targets}] -end_of_line = crlf -indent_size = 2 - -# Yaml files -[*.{yml,yaml}] -end_of_line = crlf -indent_size = 2 - -# Workflow YAML is LF: Dependabot and Actions rewrite it with LF, so declaring LF keeps it consistent instead of -# mixed. git still leaves endings alone (`* -text`). This and CI (editorconfig-checker) enforce it. Other YAML is CRLF. -[.github/workflows/*.{yml,yaml}] -end_of_line = lf - -# JSON and JSONC files -[*.{json,jsonc}] -end_of_line = crlf - -# Linux scripts -[*.sh] -end_of_line = lf - -# Windows scripts -[*.{cmd,bat,ps1}] -end_of_line = crlf - -# .NET-only below: C# and ReSharper style. Everything above is the line-ending governance -# every derived repo carries. A non-.NET repo may drop from here down. - -# C# files -[*.cs] -end_of_line = crlf -# Suppressions follow CODESTYLE.md "Analyzer Diagnostics and Suppressions": prefer a -# [SuppressMessage] attribute or the owning project's .editorconfig. Relax a rule -# repo-wide here only when it applies to every project (never a brownfield batch). -dotnet_diagnostic.IDE0055.severity = none -dotnet_analyzer_diagnostic.severity = suggestion -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = false -csharp_indent_labels = one_less_than_current -csharp_indent_switch_labels = true -csharp_new_line_before_catch = true -csharp_new_line_before_else = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all -csharp_new_line_between_query_expression_clauses = true -csharp_prefer_braces = true -csharp_prefer_simple_default_expression = true -csharp_prefer_simple_using_statement = true -csharp_prefer_static_anonymous_function = true -csharp_prefer_static_local_function = true -csharp_prefer_system_threading_lock = true -csharp_preferred_modifier_order = public,private,protected,internal,file,static,abstract,sealed,virtual,override,readonly,unsafe,volatile,async,extern,new,partial:warning -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = false -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true -csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true -csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true -csharp_style_allow_embedded_statements_on_same_line_experimental = true -csharp_style_conditional_delegate_call = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_expression_bodied_accessors = true -csharp_style_expression_bodied_constructors = true -csharp_style_expression_bodied_indexers = true -csharp_style_expression_bodied_lambdas = true -csharp_style_expression_bodied_local_functions = true -csharp_style_expression_bodied_methods = true -csharp_style_expression_bodied_operators = true -csharp_style_expression_bodied_properties = true -csharp_style_implicit_object_creation_when_type_is_apparent = true -csharp_style_inlined_variable_declaration = true -csharp_style_namespace_declarations = file_scoped -csharp_style_pattern_matching_over_as_with_null_check = true -csharp_style_pattern_matching_over_is_with_cast_check = true -csharp_style_prefer_extended_property_pattern = true -csharp_style_prefer_implicitly_typed_lambda_expression = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_local_over_anonymous_function = true -csharp_style_prefer_method_group_conversion = true -csharp_style_prefer_not_pattern = true -csharp_style_prefer_null_check_over_type_check = true -csharp_style_prefer_pattern_matching = true -csharp_style_prefer_primary_constructors = true -csharp_style_prefer_range_operator = true -csharp_style_prefer_readonly_struct = true -csharp_style_prefer_readonly_struct_member = true -csharp_style_prefer_switch_expression = true -csharp_style_prefer_top_level_statements = true -csharp_style_prefer_tuple_swap = true -csharp_style_prefer_unbound_generic_type_in_nameof = true -csharp_style_prefer_utf8_string_literals = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable -csharp_style_var_elsewhere = false -csharp_style_var_for_built_in_types = false -csharp_style_var_when_type_is_apparent = false -csharp_using_directive_placement = outside_namespace -dotnet_code_quality_unused_parameters = all -dotnet_hide_advanced_members = false -dotnet_member_insertion_location = with_other_members_of_the_same_kind -dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style -dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields -dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case -dotnet_naming_style.camel_case_underscore_style.required_prefix = _ -dotnet_naming_style.pascal_case_style.capitalization = pascal_case -dotnet_naming_style.static_prefix_style.capitalization = camel_case -dotnet_naming_style.static_prefix_style.required_prefix = s_ -dotnet_naming_symbols.constant_fields.applicable_kinds = field -dotnet_naming_symbols.constant_fields.required_modifiers = const -dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal -dotnet_naming_symbols.private_internal_fields.applicable_kinds = field -dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected -dotnet_naming_symbols.static_fields.applicable_kinds = field -dotnet_naming_symbols.static_fields.required_modifiers = static -dotnet_prefer_system_hash_code = true -dotnet_property_generation_behavior = prefer_throwing_properties -dotnet_remove_unnecessary_suppression_exclusions = none -dotnet_search_reference_assemblies = true -dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = true -dotnet_style_allow_multiple_blank_lines_experimental = true -dotnet_style_allow_statement_immediately_after_block_experimental = true -dotnet_style_coalesce_expression = true -dotnet_style_collection_initializer = true -dotnet_style_explicit_tuple_names = true -dotnet_style_namespace_match_folder = true -dotnet_style_null_propagation = true -dotnet_style_object_initializer = true -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_operators = never_if_unnecessary -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity -dotnet_style_predefined_type_for_locals_parameters_members = true -dotnet_style_predefined_type_for_member_access = true -dotnet_style_prefer_auto_properties = true -dotnet_style_prefer_collection_expression = when_types_loosely_match -dotnet_style_prefer_compound_assignment = true -dotnet_style_prefer_conditional_expression_over_assignment = true -dotnet_style_prefer_conditional_expression_over_return = true -dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed -dotnet_style_prefer_inferred_anonymous_type_member_names = true -dotnet_style_prefer_inferred_tuple_names = true -dotnet_style_prefer_is_null_check_over_reference_equality_method = true -dotnet_style_prefer_simplified_boolean_expressions = true -dotnet_style_prefer_simplified_interpolation = true -dotnet_style_qualification_for_event = false -dotnet_style_qualification_for_field = false -dotnet_style_qualification_for_method = false -dotnet_style_qualification_for_property = false -dotnet_style_readonly_field = true -dotnet_style_require_accessibility_modifiers = for_non_interface_members - -# ReSharper settings -resharper_csharp_trailing_comma_in_multiline_lists = true -resharper_csharp_var_for_built_in_types = false -resharper_csharp_var_when_type_is_apparent = false -resharper_csharp_var_when_type_is_not_apparent = false +# https://editorconfig.org + +# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names +# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview + +# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md +# https://github.com/dotnet/runtime/blob/main/.editorconfig + +# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format +# dotnet format style --verify-no-changes --severity=info --verbosity=detailed + +# Root config +root = true + +# Defaults: LF is the default, and only the CRLF exception below is declared. +# `.gitattributes` mirrors these two defaults as Git's normalization fallback. +# CI verifies the committed bytes against this file. +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +# Markdown files +[*.md] +trim_trailing_whitespace = false + +# Xml files +[*.{xml,csproj,props,targets}] +indent_size = 2 + +# Yaml files +[*.{yml,yaml}] +indent_size = 2 + +# Windows batch and command scripts: the one CRLF exception to the `[*]` LF default above. +[*.{bat,cmd}] +end_of_line = crlf + +# .NET-only below: C# and ReSharper style. Everything above is the line-ending governance +# every derived repo carries. A non-.NET repo may drop from here down. + +# C# files +[*.cs] +# Suppressions follow CODESTYLE.md "Analyzer Diagnostics and Suppressions": prefer a +# [SuppressMessage] attribute or the owning project's .editorconfig. Relax a rule +# repo-wide here only when it applies to every project (never a brownfield batch). +dotnet_diagnostic.IDE0055.severity = none +dotnet_analyzer_diagnostic.severity = suggestion +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true +csharp_prefer_braces = true +csharp_prefer_simple_default_expression = true +csharp_prefer_simple_using_statement = true +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_prefer_system_threading_lock = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,abstract,sealed,virtual,override,readonly,unsafe,volatile,async,extern,new,partial:warning +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true +csharp_style_conditional_delegate_call = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = true +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = true +csharp_style_expression_bodied_methods = true +csharp_style_expression_bodied_operators = true +csharp_style_expression_bodied_properties = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_namespace_declarations = file_scoped +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_implicitly_typed_lambda_expression = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true +csharp_style_prefer_switch_expression = true +csharp_style_prefer_top_level_statements = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_unbound_generic_type_in_nameof = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false +csharp_using_directive_placement = outside_namespace +dotnet_code_quality_unused_parameters = all +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +dotnet_naming_style.static_prefix_style.capitalization = camel_case +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_prefer_system_hash_code = true +dotnet_property_generation_behavior = prefer_throwing_properties +dotnet_remove_unnecessary_suppression_exclusions = none +dotnet_search_reference_assemblies = true +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false +dotnet_style_readonly_field = true +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# ReSharper settings +resharper_csharp_trailing_comma_in_multiline_lists = true +resharper_csharp_var_for_built_in_types = false +resharper_csharp_var_when_type_is_apparent = false +resharper_csharp_var_when_type_is_not_apparent = false diff --git a/.editorconfig-checker.json b/.editorconfig-checker.json index e019960b..a3478e97 100644 --- a/.editorconfig-checker.json +++ b/.editorconfig-checker.json @@ -1,10 +1,17 @@ -{ - "Disable": { - "Charset": true, - "Indentation": true, - "IndentSize": true, - "TrimTrailingWhitespace": true, - "InsertFinalNewline": true, - "MaxLineLength": true - } -} +{ + "Exclude": [ + "(^|/)__pycache__/", + "(^|/)\\.mypy_cache/", + "(^|/)\\.pytest_cache/", + "(^|/)\\.ruff_cache/", + "(^|/)\\.venv/" + ], + "Disable": { + "Charset": true, + "Indentation": true, + "IndentSize": true, + "TrimTrailingWhitespace": true, + "InsertFinalNewline": true, + "MaxLineLength": true + } +} diff --git a/.gitattributes b/.gitattributes index 726f427d..efb6dddb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,22 +1,7 @@ -# Default: do not normalize line endings (`* -text`); .editorconfig end_of_line rules guide what the editor writes. -# The exception pins below are git's own enforcement - they force LF for execution-sensitive classes regardless of editor. -# git config --global core.autocrlf false -# git add --renormalize . -# git ls-files --eol -* -text +# Normalize every detected text file to LF in the index and on checkout. +# `text=auto` leaves binary files byte-preserved. +* text=auto eol=lf -# Exception: scripts must stay LF regardless of the `* -text` default - a CRLF shebang breaks execution. `.editorconfig` -# covers `*.sh`, but extensionless executables (s6 service scripts, hooks) match no extension rule, so pin them here so -# git enforces LF on checkout and `--renormalize`. A repo shipping extensionless scripts adds an explicit path rule, -# e.g. for s6-overlay init: `Docker/s6-overlay/** text eol=lf`. -*.sh text eol=lf - -# Husky.Net ships an extensionless pre-commit hook with a /bin/sh shebang; pin it to LF so a CRLF can't break execution. -.husky/pre-commit text eol=lf - -# s6-overlay ships extensionless service/init scripts; pin the tree to LF so a CRLF shebang can't break execution. -Docker/s6-overlay/** text eol=lf - -# Dockerfiles must be LF - a CRLF breaks RUN heredocs and line continuations. -Dockerfile text eol=lf -*.Dockerfile text eol=lf +# Windows command scripts require CRLF. +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 221ff20b..7bb069c5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,31 +1,31 @@ -name: Bug Report -description: File a bug or issue report -title: "" -labels: ["bug", "triage"] -body: - - type: markdown - attributes: - value: | - Please note that I am not affiliated with Network Optix or Digital Watchdog, and I cannot provide support for their products. - Contact [Network Optix Support](https://support.networkoptix.com/hc/en-us/community/topics) or [Digital Watchdog Support](https://digital-watchdog.com/dw-support) for any product support issues. - Search for similar [Issues](https://github.com/ptr727/NxWitness/issues) before submitting a new issue. - For help with setting up or deploying Docker images refer to your favorite internet technical help resource. - Please submit issues in English. - Only submit issues that are related to the creation or publication of the Docker images. - - type: textarea - attributes: - label: Describe the problem? - description: What happened and what did you expect to happen. - value: | - Steps to reproduce: - - I did this. - - This happened. - - I expected this to happen. - Configuration: - - OS, platform, hardware. - - Docker image name, tag, and version. - - Deployment method and configuration. - Logs: - - Log or console output. - validations: - required: true +name: Bug Report +description: File a bug or issue report +title: "" +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Please note that I am not affiliated with Network Optix or Digital Watchdog, and I cannot provide support for their products. + Contact [Network Optix Support](https://support.networkoptix.com/hc/en-us/community/topics) or [Digital Watchdog Support](https://digital-watchdog.com/dw-support) for any product support issues. + Search for similar [Issues](https://github.com/ptr727/NxWitness/issues) before submitting a new issue. + For help with setting up or deploying Docker images refer to your favorite internet technical help resource. + Please submit issues in English. + Only submit issues that are related to the creation or publication of the Docker images. + - type: textarea + attributes: + label: Describe the problem? + description: What happened and what did you expect to happen. + value: | + Steps to reproduce: + - I did this. + - This happened. + - I expected this to happen. + Configuration: + - OS, platform, hardware. + - Docker image name, tag, and version. + - Deployment method and configuration. + Logs: + - Log or console output. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4f385e80..73d2533e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ -blank_issues_enabled: false -contact_links: - - name: Network Optix Support - url: https://support.networkoptix.com/hc/en-us/community/topics - about: Please report Nx product issues to Network Optix. - - name: Digital Watchdog Support - url: https://digital-watchdog.com/dw-support - about: Please report DW product issues to Digital Watchdog. +blank_issues_enabled: false +contact_links: + - name: Network Optix Support + url: https://support.networkoptix.com/hc/en-us/community/topics + about: Please report Nx product issues to Network Optix. + - name: Digital Watchdog Support + url: https://digital-watchdog.com/dw-support + about: Please report DW product issues to Digital Watchdog. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 55d823fa..d5a03465 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,154 +1,161 @@ -# Copilot Instructions - -Repository conventions for GitHub Copilot (and any other AI agent reading this file). - -The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract this file's runbook implements. This file is intentionally narrow: commit/PR-title conventions (summarized inline so VS Code's commit-message and PR-title generators have them) plus the GitHub Copilot Review Runbook. - -For code-style rules, see [`CODESTYLE.md`](../CODESTYLE.md) at the repo root - one guide with a General section plus per-language sections (.NET). - -Do not duplicate language-specific rules here. **Project-specific conventions and API/behavioral contracts also belong in [AGENTS.md](../AGENTS.md), not here** - this file is intentionally limited to the inline commit/PR-title summary and the GitHub Copilot Review Runbook. Non-Copilot agents (Claude Code, Codex, Cursor, ...) are not directed to this file and don't read it by default, so any rule a reviewer must honor has to live in `AGENTS.md` to be provider-independent. - -## Commit Messages and Pull Request Titles - -Summarized for VS Code's generators; the full rules, rationale, and examples are in [AGENTS.md "Pull Request Title and Commit Message Conventions"](../AGENTS.md#pull-request-title-and-commit-message-conventions). - -- Imperative subject, <= 72 characters, no trailing period; optional blank-line-separated body for the non-obvious *why*. -- US English, title case with lowercase short bind words; no vague titles, no `Co-Authored-By:` unless asked, no release-bump magnitude (NBGV handles versioning). Dependabot's `Bump X from Y to Z` titles are fine. -- develop PRs squash-merge (`gh pr merge --squash`), main PRs merge-commit (`--merge`); a mismatched flag is rejected by branch protection. - -## GitHub Copilot Review Runbook - -> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `` placeholder with the PR number. - -Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. - -### Triggering and Polling - -Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This drives the loop end-to-end without a UI hand-off. - -**A review with no inline comments is still a completed review - not a failure, and not a reason to ask the maintainer to re-trigger.** Copilot very often posts a single formal review (GraphQL `state: COMMENTED`) whose body ends with "...reviewed N of N changed files ... and generated no comments" and adds **zero** inline threads. That review carries the head `commit.oid` and fully satisfies the loop - it is the clean-pass success case. Never read "no inline comments" as "the review didn't run," and never re-request or escalate to the maintainer because comments are absent. - -**Round 1 is normally auto-seeded - poll for it before trying to self-trigger.** Auto-review-on-open supplies the first review with no `botIds` call needed, but it can lag one to three minutes. After opening a PR (or the first push), **poll** for a Copilot review on the head SHA (see [Verify Review Covered Current Head](#verify-review-covered-current-head)) before concluding none ran. The `requestReviews` mutation below is for **re-requesting on later pushes** (a new head SHA); by then a prior review exists, so its bot node id is readable. A missing bot node id on round 1 therefore means "the auto-review has not landed yet - wait and poll," **not** "ask the maintainer to kick it off." - -> **The reviewer login differs by API.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. - -```sh -# 1. PR node id + the Copilot reviewer's bot node id (read from any existing -# Copilot review; the reviewer login is `copilot-pull-request-reviewer`). -PR_NODE=$(gh pr view --json id --jq '.id') -BOT_ID=$(gh api graphql -f query=' -{ - repository(owner: "ptr727", name: "NxWitness") { - pullRequest(number: ) { - reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } } - } - } -}' --jq '[.data.repository.pullRequest.reviews.nodes[] - | select(.author.login == "copilot-pull-request-reviewer") - | .author.id] | first') - -# 2. Re-request a Copilot review on the current head. -gh api graphql -f query=' -mutation($pr: ID!, $bot: ID!) { - requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) { - pullRequest { id } - } -}' -F pr="$PR_NODE" -F bot="$BOT_ID" -``` - -The bot node id is read from an existing Copilot **formal** review (`pullRequest.reviews`), so step 1 needs at least one prior formal review on the PR - the auto-review-on-open normally supplies the first one (it may have **no inline comments**; that still counts, and its bot node id is still readable). Poll for it (give auto-review-on-open a few minutes) before deciding it is missing. If Copilot posted **only an issue comment** and no formal review, the head is covered but `reviews` yields no bot node id - read the id from the Copilot issue comment's author by querying the PR's issue comments in GraphQL (`pullRequest.comments` -> author `... on Bot { id }`), or request `Copilot` once through the GitHub PR UI to produce a formal review. Manual UI seeding is the fallback specifically when no formal review exists to read the id from; then use the mutation for every subsequent re-request. - -**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. - -Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead): - -- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op. -- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422. - -### Verify Review Covered Current Head - -Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both. - -```sh -PR_HEAD=$(gh pr view --json headRefOid --jq '.headRefOid') - -# 1. Formal review - exact SHA match. -gh pr view --json reviews --jq \ - '.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \ - | grep -q "$PR_HEAD" && echo "covered via formal review" - -# 2. Issue comment - show the most recent Copilot comment for manual -# confirmation. This is the REST API, so the login carries the `[bot]` suffix. -gh api repos/ptr727/NxWitness/issues//comments --jq \ - '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}' -``` - -Coverage is confirmed when (1) exits 0 - **a formal review with no inline comments still satisfies path (1)**, because coverage is about the head SHA, not the comment count. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. - -### Bounded Retry Workflow - -This path is only for a **genuinely missing** review - no Copilot review (formal *or* issue comment) covers the current head SHA after polling. A review that covered the head but produced no comments is a clean pass, not a missing review; do not enter this retry path for it. - -If a review did not run on the current head, retry: - -1. Wait briefly and check head-SHA coverage (see above). -1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops. -1. Retry up to two more times (three total). -1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted. - -### Reply and Thread Resolution Workflow - -List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: ""` to retrieve the next page: - -```sh -gh api graphql -f query=' -{ - repository(owner: "ptr727", name: "NxWitness") { - pullRequest(number: ) { - reviewThreads(first: 100) { - nodes { - id isResolved path - comments(first: 1) { nodes { author { login } body } } - } - pageInfo { hasNextPage endCursor } - } - } - } -}' | jq ' - .data.repository.pullRequest.reviewThreads | - (.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"), - (.nodes[] | select(.isResolved == false)) -' -``` - -Reply on a thread, then resolve it: - -```sh -gh api graphql -f query=' -mutation($threadId: ID!, $body: String!) { - addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) { - comment { id } - } -}' -F threadId="PRRT_..." -F body="Fixed in : ." - -gh api graphql -f query=' -mutation($threadId: ID!) { - resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } } -}' -F threadId="PRRT_..." -``` - -Issue-level Copilot comments (those in `issues//comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible. - -Reply-body conventions: - -- Accepted bug/style fix: include fixing commit SHA and a one-line summary. -- Declined style comment: cite the rule (AGENTS.md or the CODESTYLE.md language section) and the existing-tree precedent. -- Declined architecture proposal: one-sentence rationale. - -After the final push, sweep-resolve stale older threads for removed code paths. - -## When in Doubt - -Read [AGENTS.md](../AGENTS.md) for this repo's conventions. For code-style rules, [`CODESTYLE.md`](../CODESTYLE.md) (its General section plus the relevant language section) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself. - -**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream). +# Copilot Instructions + +Repository conventions for GitHub Copilot (and any other AI agent reading this file). + +The **entry point is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for where each rule lives, then the section it names in [`GOVERNANCE.md`](../GOVERNANCE.md), including the [PR Review Etiquette](../GOVERNANCE.md#pr-review-etiquette) review-loop contract this file's runbook implements. When performing code review, load and follow the `code-review` skill in [`.github/skills/code-review/SKILL.md`](./skills/code-review/SKILL.md), then load every language, documentation, or workflow skill that it selects for the changed files. GitHub Copilot reads these files from the pull request's head branch, so review the instructions in that tree. This file is intentionally narrow: commit/PR-title conventions (summarized inline so VS Code's commit-message and PR-title generators have them) plus the GitHub Copilot Review Runbook. + +For code-style rules, see [`CODESTYLE.md`](../CODESTYLE.md) at the repo root - one guide with a General section plus per-language sections (.NET). + +Do not duplicate language-specific rules here. **Project-specific conventions and API/behavioral contracts belong in [`ARCHITECTURE.md`](../ARCHITECTURE.md) or [`OPERATIONS.md`](../OPERATIONS.md), not here and not in `AGENTS.md`** - that file is a router carrying three fixed sections, so a durable rule written into it is drift. This file is intentionally limited to the inline commit/PR-title summary and the GitHub Copilot Review Runbook. Non-Copilot agents (Claude Code, Codex, Cursor, ...) are not directed to this file and don't read it by default, so any rule a reviewer must honor has to live in one of the agent-agnostic documents to be provider-independent. + +## Commit Messages and Pull Request Titles + +Summarized for VS Code's generators; the full rules, rationale, and examples are in [GOVERNANCE.md "Pull Request Title and Commit Message Conventions"](../GOVERNANCE.md#pull-request-title-and-commit-message-conventions). + +- Imperative subject, <= 72 characters, no trailing period; optional blank-line-separated body for the non-obvious *why*. +- US English, title case with lowercase short bind words; no vague titles, no `Co-Authored-By:` unless asked, no release-bump magnitude (NBGV handles versioning). Dependabot's `Bump X from Y to Z` titles are fine. +- develop PRs squash-merge (`gh pr merge --squash`), main PRs merge-commit (`--merge`); a mismatched flag is rejected by branch protection. + +## Reviewing Carried Fleet Content + +Follow the fidelity declared for the file. A byte-locked reference to shared infrastructure that +this repository does not carry is intentional, not a broken link. Raise substantive defects in +canonical content, but locate the fix at its canonical source instead of proposing a local edit +that its fidelity rejects. + +## GitHub Copilot Review Runbook + +> This runbook implements the [GOVERNANCE.md "PR Review Etiquette"](../GOVERNANCE.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `` placeholder with the PR number. + +Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [GOVERNANCE.md -> PR Review Etiquette](../GOVERNANCE.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. + +### Triggering and Polling + +Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This drives the loop end-to-end without a UI hand-off. + +**A review with no inline comments is still a completed review - not a failure, and not a reason to ask the maintainer to re-trigger.** Copilot very often posts a single formal review (GraphQL `state: COMMENTED`) whose body ends with "...reviewed N of N changed files ... and generated no comments" and adds **zero** inline threads. That review carries the head `commit.oid` and fully satisfies the loop - it is the clean-pass success case. Never read "no inline comments" as "the review didn't run," and never re-request or escalate to the maintainer because comments are absent. + +**Round 1 is normally auto-seeded - poll for it before trying to self-trigger.** Auto-review-on-open supplies the first review with no `botIds` call needed, but it can lag one to three minutes. After opening a PR (or the first push), **poll** for a Copilot review on the head SHA (see [Verify Review Covered Current Head](#verify-review-covered-current-head)) before concluding none ran. The `requestReviews` mutation below is for **re-requesting on later pushes** (a new head SHA); by then a prior review exists, so its bot node id is readable. A missing bot node id on round 1 therefore means "the auto-review has not landed yet - wait and poll," **not** "ask the maintainer to kick it off." + +> **The reviewer login differs by API.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. + +```sh +# 1. PR node id + the Copilot reviewer's bot node id (read from any existing +# Copilot review; the reviewer login is `copilot-pull-request-reviewer`). +PR_NODE=$(gh pr view --json id --jq '.id') +BOT_ID=$(gh api graphql -f query=' +{ + repository(owner: "ptr727", name: "NxWitness") { + pullRequest(number: ) { + reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } } + } + } +}' --jq '[.data.repository.pullRequest.reviews.nodes[] + | select(.author.login == "copilot-pull-request-reviewer") + | .author.id] | first') + +# 2. Re-request a Copilot review on the current head. +gh api graphql -f query=' +mutation($pr: ID!, $bot: ID!) { + requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) { + pullRequest { id } + } +}' -F pr="$PR_NODE" -F bot="$BOT_ID" +``` + +The bot node id is read from an existing Copilot **formal** review (`pullRequest.reviews`), so step 1 needs at least one prior formal review on the PR - the auto-review-on-open normally supplies the first one (it may have **no inline comments**; that still counts, and its bot node id is still readable). Poll for it (give auto-review-on-open a few minutes) before deciding it is missing. If Copilot posted **only an issue comment** and no formal review, the head is covered but `reviews` yields no bot node id - read the id from the Copilot issue comment's author by querying the PR's issue comments in GraphQL (`pullRequest.comments` -> author `... on Bot { id }`), or request `Copilot` once through the GitHub PR UI to produce a formal review. Manual UI seeding is the fallback specifically when no formal review exists to read the id from; then use the mutation for every subsequent re-request. + +**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. + +Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead): + +- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op. +- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422. + +### Verify Review Covered Current Head + +Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both. + +```sh +PR_HEAD=$(gh pr view --json headRefOid --jq '.headRefOid') + +# 1. Formal review - exact SHA match. +gh pr view --json reviews --jq \ + '.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \ + | grep -q "$PR_HEAD" && echo "covered via formal review" + +# 2. Issue comment - show the most recent Copilot comment for manual +# confirmation. This is the REST API, so the login carries the `[bot]` suffix. +gh api repos/ptr727/NxWitness/issues//comments --jq \ + '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}' +``` + +Coverage is confirmed when (1) exits 0 - **a formal review with no inline comments still satisfies path (1)**, because coverage is about the head SHA, not the comment count. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. + +### Bounded Retry Workflow + +This path is only for a **genuinely missing** review - no Copilot review (formal *or* issue comment) covers the current head SHA after polling. A review that covered the head but produced no comments is a clean pass, not a missing review; do not enter this retry path for it. + +If a review did not run on the current head, retry: + +1. Wait briefly and check head-SHA coverage (see above). +1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops. +1. Retry up to two more times (three total). +1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted. + +### Reply and Thread Resolution Workflow + +List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: ""` to retrieve the next page: + +```sh +gh api graphql -f query=' +{ + repository(owner: "ptr727", name: "NxWitness") { + pullRequest(number: ) { + reviewThreads(first: 100) { + nodes { + id isResolved path + comments(first: 1) { nodes { author { login } body } } + } + pageInfo { hasNextPage endCursor } + } + } + } +}' | jq ' + .data.repository.pullRequest.reviewThreads | + (.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"), + (.nodes[] | select(.isResolved == false)) +' +``` + +Reply on a thread, then resolve it: + +```sh +gh api graphql -f query=' +mutation($threadId: ID!, $body: String!) { + addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) { + comment { id } + } +}' -F threadId="PRRT_..." -F body="Fixed in : ." + +gh api graphql -f query=' +mutation($threadId: ID!) { + resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } } +}' -F threadId="PRRT_..." +``` + +Issue-level Copilot comments (those in `issues//comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible. + +Reply-body conventions: + +- Accepted bug/style fix: include fixing commit SHA and a one-line summary. +- Declined style comment: cite the rule (the GOVERNANCE.md section or the CODESTYLE.md language section) and the existing-tree precedent. +- Declined architecture proposal: one-sentence rationale. + +After the final push, sweep-resolve stale older threads for removed code paths. + +## When in Doubt + +Read [AGENTS.md](../AGENTS.md) for where each rule lives, and [`GOVERNANCE.md`](../GOVERNANCE.md) for the rule text itself. For code-style rules, [`CODESTYLE.md`](../CODESTYLE.md) (its General section plus the relevant language section) is authoritative. This repo's own design is in [`ARCHITECTURE.md`](../ARCHITECTURE.md) and how it is run in [`OPERATIONS.md`](../OPERATIONS.md). Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself. + +A rule that is missing, out of date, or that bit this repo and would bite the next is worth raising where the rule is maintained rather than only patching here, since a local fix leaves every other reader with the same gap. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4b2306c7..3eb8f80d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,97 +1,97 @@ -# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file -# -# Every ecosystem appears **twice**: once with `target-branch: "main"` -# and once with `target-branch: "develop"`. Dependabot will open -# parallel PRs against each branch, so both stay current on -# dependency versions independently of the develop -> main release -# cadence. -# -# Why dual-target and not develop-only: -# - `develop` is the integration branch and ships content forward to -# `main` through merge-commit releases, but the time between releases -# can be long (a feature branch may sit on develop for weeks). -# - Push-distribution channels (e.g. Docker Hub, distros that pull from -# main) consume `main` directly. If `main` only got dependency bumps -# via the next develop -> main release, those channels would ship -# outdated code in the interim. -# - Codegen workflows take the same dual-target shape for the same -# reason - see .github/workflows/run-codegen-pull-request-task.yml. -# -# The merge-bot's `case` statement in -# .github/workflows/merge-bot-pull-request.yml dispatches the merge -# method per base ref (squash on develop, merge on main) so both bases -# auto-merge cleanly. `develop` remains strictly forward-only: there -# are no main -> develop back-merges; each branch absorbs its own -# Dependabot PRs and codegen PRs independently. -# -# Security update PRs (CVE-driven) are opened by Dependabot against -# the repo default branch (`main`) regardless of any `target-branch` -# config - the `case` statement handles them in the same code path. -version: 2 -updates: - - # ----- nuget ----- - - - package-ecosystem: "nuget" - target-branch: "main" - directory: "/" - schedule: - interval: "daily" - groups: - nuget-deps: - patterns: - - "*" - - - package-ecosystem: "nuget" - target-branch: "develop" - directory: "/" - schedule: - interval: "daily" - groups: - nuget-deps: - patterns: - - "*" - - # ----- github-actions ----- - - - package-ecosystem: "github-actions" - target-branch: "main" - directory: "/" - schedule: - interval: "daily" - groups: - actions-deps: - patterns: - - "*" - - - package-ecosystem: "github-actions" - target-branch: "develop" - directory: "/" - schedule: - interval: "daily" - groups: - actions-deps: - patterns: - - "*" - - # ----- docker (Docker/*.Dockerfile) ----- - - - package-ecosystem: "docker" - target-branch: "main" - directory: "/Docker" - schedule: - interval: "daily" - groups: - docker-deps: - patterns: - - "*" - - - package-ecosystem: "docker" - target-branch: "develop" - directory: "/Docker" - schedule: - interval: "daily" - groups: - docker-deps: - patterns: - - "*" +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +# +# Every ecosystem appears **twice**: once with `target-branch: "main"` +# and once with `target-branch: "develop"`. Dependabot will open +# parallel PRs against each branch, so both stay current on +# dependency versions independently of the develop -> main release +# cadence. +# +# Why dual-target and not develop-only: +# - `develop` is the integration branch and ships content forward to +# `main` through merge-commit releases, but the time between releases +# can be long (a feature branch may sit on develop for weeks). +# - Push-distribution channels (e.g. Docker Hub, distros that pull from +# main) consume `main` directly. If `main` only got dependency bumps +# via the next develop -> main release, those channels would ship +# outdated code in the interim. +# - Codegen workflows take the same dual-target shape for the same +# reason - see .github/workflows/run-codegen-pull-request-task.yml. +# +# The merge-bot's `case` statement in +# .github/workflows/merge-bot-pull-request.yml dispatches the merge +# method per base ref (squash on develop, merge on main) so both bases +# auto-merge cleanly. `develop` remains strictly forward-only: there +# are no main -> develop back-merges; each branch absorbs its own +# Dependabot PRs and codegen PRs independently. +# +# Security update PRs (CVE-driven) are opened by Dependabot against +# the repo default branch (`main`) regardless of any `target-branch` +# config - the `case` statement handles them in the same code path. +version: 2 +updates: + + # ----- nuget ----- + + - package-ecosystem: "nuget" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + nuget-deps: + patterns: + - "*" + + - package-ecosystem: "nuget" + target-branch: "develop" + directory: "/" + schedule: + interval: "daily" + groups: + nuget-deps: + patterns: + - "*" + + # ----- github-actions ----- + + - package-ecosystem: "github-actions" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + actions-deps: + patterns: + - "*" + + - package-ecosystem: "github-actions" + target-branch: "develop" + directory: "/" + schedule: + interval: "daily" + groups: + actions-deps: + patterns: + - "*" + + # ----- docker (Docker/*.Dockerfile) ----- + + - package-ecosystem: "docker" + target-branch: "main" + directory: "/Docker" + schedule: + interval: "daily" + groups: + docker-deps: + patterns: + - "*" + + - package-ecosystem: "docker" + target-branch: "develop" + directory: "/Docker" + schedule: + interval: "daily" + groups: + docker-deps: + patterns: + - "*" diff --git a/.github/skills/add-host-tool/SKILL.md b/.github/skills/add-host-tool/SKILL.md new file mode 100644 index 00000000..22b10df5 --- /dev/null +++ b/.github/skills/add-host-tool/SKILL.md @@ -0,0 +1,46 @@ +--- +name: add-host-tool +description: >- + Adds or changes a managed host tool across the ptr727/ProjectTemplate fleet contract, Linux and + Windows installers, platform documentation, and tests. Use this whenever adding, removing, + renaming, or changing the source, probe, version floor, install, report, upgrade, or dry-run + behavior of a tool in host-setup or spec/host-tools.json. Triggers even when the request names + only one platform, because a required fleet tool needs an executable remedy everywhere it + applies and native verification must stay on the platform being tested. +--- + +# Add Host Tool + +## Establish the Contract + +1. Read the issue and all follow-up comments before choosing a source or package identifier. +2. Add the tool to `spec/host-tools.json` in name order. +3. Use the executable's real version banner for the probe and pattern. +4. Set a floor only when it is measured or anchored to every supported distribution. +5. Provide `source` and executable `remedy` entries for every applicable platform. + +## Implement Each Platform + +- Keep the existing named-tool interface and default selection behavior. +- Prefer the distribution package when it meets the floor. +- Use the platform's established package manager and official package identifier. +- Keep install and upgrade idempotent. +- Before an apt-managed install, detect and remove an unowned downloaded copy that shadows it. +- Before a downloaded install, detect and remove a conflicting package-managed copy. +- Preserve report, list, explicit selection, install, upgrade, reinstall, and dry-run behavior. +- Do not test a Windows mutation on Linux or a Linux mutation on Windows. + +When a platform is unavailable, verify its registry and tests without claiming a native install. Hand off the exact native commands and expected observations to the operator. + +## Update the Complete Surface + +Update the platform installers, `spec/host-tools.json`, `docs/host-setup.md`, and the applicable platform READMEs. Update installer and host-gate tests for selection, reporting, installation, upgrade, and dry-run behavior. Sweep prose that describes tool sources or the managed set. + +## Verify + +1. Run the spec validator and the focused installer and host-gate tests. +2. Run the repository's formatting, lint, type, and test gates required by the changed files. +3. On the current native platform, exercise list and report first. +4. Exercise install and upgrade dry runs. +5. Apply the install, repeat it to prove idempotence, and run the host gate. +6. Record untested platforms explicitly and leave cross-platform verification open. diff --git a/.github/skills/agent-conduct/SKILL.md b/.github/skills/agent-conduct/SKILL.md new file mode 100644 index 00000000..060267c4 --- /dev/null +++ b/.github/skills/agent-conduct/SKILL.md @@ -0,0 +1,47 @@ +--- +name: agent-conduct +description: >- + Surfaces the ptr727/ProjectTemplate fleet's conduct rules at the three decision moments they are violated: about to claim work is done, verified, green, or fixed, about to proceed on an assumption the user could cheaply confirm, and a failure or review finding just surfaced a durable lesson. Use this whenever about to report success or completion of any task, whenever about to pick a default, guess an intent, or resolve an ambiguity without asking, whenever work is blocked on a decision or authorization only the user can give, and whenever an incident, a wrong answer, or a repeated correction just taught something a future session must honor. Deliberately narrow: the carried AGENTS.md sections are the always-on layer, and this skill fires at the moments rather than duplicating them, so do not load it as general background. Where a sibling skill owns the moment, it wins: git-commit-conventions for committing, pr-review-conduct for review and merge claims, comment-and-doc-style for prose. The GOVERNANCE.md sections this skill summarizes keep the full rules. +--- + +# Agent Conduct + +## Why This Exists + +The fleet's conduct rules (verification before claiming done, asking instead of assuming, recording lessons) lived only in doc sections nothing surfaced at the moment of violation, so they were honored by whoever happened to have read them recently. This skill is the decision-moment surface. The full rules stay in `GOVERNANCE.md` ("Verification Discipline", "Communicating with the User", "Durable Knowledge and Self-Improvement"), which keeps authority, and in the carried `AGENTS.md` "Context and Delegation Discipline" section, which is the always-on layer. + +## Before Claiming Done + +Read `GOVERNANCE.md` "Verification Discipline" before reporting success on anything non-trivial. Its unifying property: every failure it lists is green. The checks that bind here: + +- **A green check is not evidence the work happened.** A skipped job and a passing job are indistinguishable in an aggregated required check, so confirm from the log that the job ran and produced what it promises. +- **Locate every check the change owes before running any**, from what the repository declares (`OPERATIONS.md` "Local Verification" beside the workflows), not from what the pipeline happens to run, since part of a contract is routinely unreachable from a runner and green is then the precise signal it was skipped. +- **Run the repo's whole lint gate before every push**, not the parts that look relevant, because the tool most likely to catch a change is often the one it seems least about. +- **A launched process is not a result.** Report the output the wait produced, and where it produced none, that absence is the report. Never name an external cause the record does not carry. +- **A local clone is not the branch it names.** Fetch immediately before reading, or read the live ref, and name the ref and commit in any finding a local read produced. +- **A checkout this session did not create is not ground truth.** One found already sitting on disk may belong to another concurrent session, sit on a stale fetch or an unexpected branch, or hold unreviewed uncommitted edits. Clone fresh or read the live API instead of trusting `git status`/`git remote -v` run against a pre-existing checkout. +- **A "does not exist" claim names the branch it was checked against.** A worktree's default branch is not necessarily the one the content lives on: in-flight content on a `release`-model repo lands on `develop` before `main`, per `GOVERNANCE.md` "Branching Model," so check that branch before reporting anything absent repo-wide. +- **A `raw.githubusercontent.com` 404 does not distinguish a private repository from a missing file.** Where visibility is not confirmed public, read content via `gh api "repos///contents/?ref="`, capturing the result before decoding it (`content=$(gh api ... --jq '.content') && printf '%s' "$content" | base64 -d`) rather than piping straight into `base64 -d`, whose own exit status is all a direct pipe reports, letting a failed fetch decode as an empty success. Never `2>&1` either form, which corrupts the decode with the error text instead of the payload. Verify the ref resolves before reading either failure as proof the content itself does not exist. +- **A test asserts the mechanism it names, and a gate has to be watched failing.** A case that passes for an incidental reason is worse than no case, because it is later cited as evidence. +- **Platform-specific code is verified only on the platform it runs on.** Reasoning about PowerShell, macOS, or WSL-specific behavior from a different host is not verification, however closely it matches an already-tested equivalent elsewhere. State an untested structural match as exactly that, never in the words used for a tested fact, and when no agent in the loop has access to the target platform, say so and defer or ship it labeled unverified. +- **PR-bound work runs `local-strict-review` before the claim.** Claiming a unit of work done, verified, green, or fixed for work that will become, or already is, a pull request means running `local-strict-review` against the branch's diff first, before a PR-hosted reviewer finds the same gap. + +Claims about a pull request being reviewed, clean, or mergeable are owned by the `pr-review-conduct` skill, and claims that a commit landed by `git-commit-conventions`. + +## Before Assuming + +- **Ask when the user can cheaply confirm.** An assumption that saves one question and is wrong costs the rework plus the trust, so a genuine ambiguity in intent, scope, or authorization is raised, not resolved by picking the likelier reading. Rules that already answer the question (the committed instruction set) are not ambiguity, so read them first rather than asking what they state. +- **Raise blocked work as a direct interactive prompt** at the point the work stops, per `GOVERNANCE.md` "Communicating with the User": the blocked item is the message, the options offered are the actions themselves, and a handoff buried in a summary paragraph is a handoff that did not happen. Numbered lists are the fallback where no prompt mechanism exists. +- **References are clickable where they are read**: a pull request, issue, or commit on a Markdown surface is a Markdown link, and on a surface that renders neither, a bare `#123` with the link in the message before the prompt. +- **Capability is not permission.** A token's reach, a tool that happens to work, or a similar grant in a past session authorizes nothing, and the irreversible step (merge, publish, release, delete) stays the maintainer's. + +## When a Failure Surfaces a Lesson + +- **Durable knowledge lands in the committed docs, not in agent memory**, as part of the change that surfaced it, per `GOVERNANCE.md` "Durable Knowledge and Self-Improvement". Memory does not survive a new session or machine, so it holds only environment nuance and in-flight state. +- **Where the governing doc is carried from the hub, file the finding against `ptr727/ProjectTemplate`** rather than only patching it locally. A local fix leaves every sibling repo with the same trap. Search open and closed issues first, then update the matching issue or file a new one. +- **A review flags an instance, so fix the class**: sweep for the siblings before replying, because reviewers sample rather than enumerate. +- **A rule that keeps needing restating** is usually a stale or missing skills install, so run `python3 scripts/skills_install.py --report` from a hub checkout (the `fleet-conformance-check` skill) before concluding the rule does not exist. + +## Delegation, in One Paragraph + +The always-on rules live in `AGENTS.md` "Context and Delegation Discipline" and are not restated here. The two that intersect conduct: brief a subagent so it never needs a governance file, since anything it must honor has to be in its prompt, and never tier down the seat holding the judgment, because governance wording and the decision to decline a review finding are fleet-wide and durable when wrong. diff --git a/.github/skills/audit-a-repo/SKILL.md b/.github/skills/audit-a-repo/SKILL.md new file mode 100644 index 00000000..5cd0c133 --- /dev/null +++ b/.github/skills/audit-a-repo/SKILL.md @@ -0,0 +1,37 @@ +--- +name: audit-a-repo +description: >- + Drives AUDIT.md's read-only measurement of a named ptr727 fleet repo against the fleet ground truth, ending in a committed report, never an edit to the repo being measured. Use this whenever asked to audit, measure, or verify conformance of a named repo, to judge a conformance claim someone else made, or to decide whether an onboarding is actually complete. Run from a hub checkout of ptr727/ProjectTemplate against the named target. Triggers even when the repo believes it is conformant, because conformance asserted without a committed report is conformance nobody can check, and that is the case most often skipped. This completes the procedure triangle: standup-a-repo creates a repo, resync-a-repo applies findings to one already stood up, and this skill measures, while fleet-conformance-check is the in-repo self-check with no named target and no standing hub checkout. AUDIT.md keeps authority over the procedure, this skill is the summary that routes into it. +--- + +# Audit a Repo + +## Why This Exists + +The audit is the fleet's measurement procedure, and the two failure shapes it guards against are both silent: a repo judged conformant with no committed evidence, and an audit that quietly edits what it was supposed to measure. `AUDIT.md` in the hub is the procedure and keeps authority. This skill carries the rules that get skipped in practice and says which section owns each step. + +## Before Measuring Anything + +- **Route first.** A repo with no carried instruction set, or a partial one, has a baseline that never arrived rather than drift to report, so it goes to `STANDUP.md` sections 1A and 2 first (`AUDIT.md` section 0). Auditing it anyway produces a report that is all absences and reads as catastrophe. +- **Verify the host.** Run `python3 scripts/host_gate.py --repo ` from the hub checkout before any hub tool, and pass `--repo`, since a bare run skips the target's own `host-tools.json` overlay. A stale tool answers `--version`, looks healthy, and produces a wrong answer. +- **Read `main` as ground truth**, for both workflow models, and read `develop` only to detect divergence (`AUDIT.md` section 1). An `operational` repo's `develop` is mid-flight by design, so conformance work sitting there is un-promoted work, not a defect, and it counts when it reaches `main`. Use `spec/audit.py --branch ` to preview in-flight work, which stamps the override so the finding cannot be mistaken for one against ground truth. + +## Measuring + +- **Resolve the repo's types from `registry/repos.json`** and classify a `classificationPending` entry from the tree (`AUDIT.md` section 2). The applicability gate is `WORKFLOW.md` section 1: a check governing an absent construct is N/A, excluded from the verdict, and never a defect (`AUDIT.md` section 3). +- **Know what the runner does and does not prove.** `spec/audit.py` mechanizes the deterministic subset only: settings, rulesets, secret names, file and section presence, verbatim hashing, interface wiring, Dependabot coverage, branch facts. It evaluates no check under a type in `spec/project-types.json`, so every per-type check is judged by hand, and a clean run is no evidence for them (`AUDIT.md` section 4). Silence from a tool that was never looking reads exactly like a pass. +- **Judge letter and intent per check** and keep the vocabulary: letter miss with intent satisfied is a drift finding, both missing is a defect, and operational is binary over the applicable set (`AUDIT.md` sections 4 and 7). Do not invent a parallel scheme. +- **Assert the Actions implement `WORKFLOW.md`** by outcome, not by matching catalog snippets byte for byte: the 5A static audit with a `file:line` citation per applicable guarantee, then the 5B trace scenarios (`AUDIT.md` section 5). The `workflow-ci-contract` skill summarizes that contract. +- **Check live settings, rulesets, and secrets from a hub checkout at `main`** with `AUDIT.md` section 6. Run `repo-config/configure.sh check` with the target repository and model for settings and rulesets, and `spec/audit.py [RepoName]` for secrets, rather than constructing a local comparison. The hub payloads are the only repository-configuration source. + +## Reporting + +- **Write `reports//audit.md` from `reports/_template.md`**, findings ranked most severe first, each with the `file:line` it was judged against, and quote the run stamp, since findings are a point-in-time snapshot (`AUDIT.md` section 8). +- **The hub authors the report.** A downstream repo never opens a hub pull request to write its own, which would be self-certification. Downstream context goes into issues filed against the hub instead. +- **Generate a convergence issue, never compose one**: `spec/audit.py --issue ` emits it from live findings. An agent picking such an issue up re-runs the audit first and acts on the live result, not the pasted findings. +- **Reconcile registry `driftNotes` in the same pass**: a resolved deviation's note is deleted, not left describing finished work, and a note naming a check id is retired by a person, not by a run (`AUDIT.md` section 8). +- **Stale-versus-modified classification needs a full hub clone with git history.** Without one, compare against the current hub canonical on `main`, which decides current-match only. + +## After the Report + +Measuring and fixing are separate phases. Converging is `AUDIT.md` section 10: fixes ship as pull requests on the target repo, one focused pull request per drift class, the Copilot loop driven to green per the `pr-review-conduct` skill, and the maintainer merges. For a repo already stood up, `RESYNC.md` sequences the findings, since order matters (a deletion lands before the re-vendor that would refresh it). Systemic drift shared by many repos is fixed in the hub spec, not hand-patched per repo, and spec questions are escalated rather than resolved silently (`AUDIT.md` section 9). diff --git a/.github/skills/carried-instruction-file-guard/SKILL.md b/.github/skills/carried-instruction-file-guard/SKILL.md new file mode 100644 index 00000000..47f88033 --- /dev/null +++ b/.github/skills/carried-instruction-file-guard/SKILL.md @@ -0,0 +1,31 @@ +--- +name: carried-instruction-file-guard +description: >- + Stops a blind overwrite of a downstream repo's AGENTS.md, GOVERNANCE.md, CODESTYLE.md, or WORKFLOW.md when resyncing or updating it to match the ptr727/ProjectTemplate hub template. Use this whenever about to edit, replace, re-vendor, or sync-to-match-the-hub any of those four files in a repository that is not ProjectTemplate itself, or whenever asked to bring a repo's instruction set up to date, run a conformance sweep, or fix drift against the hub. Triggers even when the request sounds routine, such as copying the hub's AGENTS.md over or resyncing a repo's docs, because that phrasing is exactly how a real incident happened, where a downstream repo's local rules were silently deleted by a full-file overwrite. Do not skip this just because the task looks mechanical. +--- + +# Carried Instruction File Guard + +## Why this exists + +A downstream repo's `AGENTS.md`/`GOVERNANCE.md`/`CODESTYLE.md`/`WORKFLOW.md` can hold two different kinds of content mixed in one file: sections that are stale copies of the hub's fleet-wide rules, and local rules the repo wrote for a fault the fleet has never seen elsewhere. Re-vendoring the hub's canonical version over the whole file deletes the second kind silently, because nothing about the diff looks wrong. This has actually happened: a resync replaced a repo's `AGENTS.md` wholesale with the hub's, and the repo's own local additions were gone with no error, no warning, and no review comment calling it out. + +The fix is not "be careful." Being careful is what failed the first time. The fix is a mechanical check you run before any overwrite touches one of these four files, every time, regardless of how routine the request sounds. + +## Before you touch any of these four files + +1. **Check whether the file's content is declared `verbatim` or `intent`.** The hub's `spec/section-model.md` (fetch it from a hub checkout, `github.com/ptr727/ProjectTemplate`, if you don't have one) names, section by section, which parts of `AGENTS.md` and `GOVERNANCE.md` are universal fleet law (safe to byte-match against the hub) and which describe the repo itself (never safe to overwrite from another repo). `CODESTYLE.md` and `WORKFLOW.md` are carried whole at `intent` fidelity, judged by meaning, not hashed. +2. **If any part of the file is `intent`, or if the file predates a clean split into hub-governed sections, do not diff-and-replace. Probe instead.** For each rule or paragraph in the current file that is not obviously boilerplate: + - Pick the phrase in it that is most peculiar to this repo, not generic governance vocabulary. A rule about "always sign commits" is generic. A rule about "this repo's Docker image pins Alpine 3.19 because 3.20 broke the s6 supervisor" is peculiar. + - Grep the hub's canonical copy of the same file for that peculiar phrase. + - **Absent from the hub canonical means it is a local addition.** It is never dropped because it looks similar to something else, and never dropped because a merge or overwrite would be simpler without it. +3. **A local addition found by the probe gets a destination, not a deletion.** Either it names a rule that should apply fleet-wide (flag it for the maintainer to promote into the hub), or it is genuinely specific to this repo and moves to the repo's own topical doc before the carried file is touched: `CODESTYLE.md` for a language/formatting convention, `ARCHITECTURE.md` for a design decision, `OPERATIONS.md` for a runbook or operational note, `TODO.md` for backlog. Move it, confirm it is not lost, and only then proceed with the carry. +4. **Do not trust a similarity or word-overlap check for step 2.** A repo-specific rule written in ordinary governance language reads as a reworded duplicate of an unrelated hub rule to that kind of check, and it will confidently tell you the local content is redundant when it is not. Exact phrase presence or absence is the only check that has held up. + +## What is actually safe to overwrite without this procedure + +A section `spec/section-model.md` names as `verbatim`, in a file that is already cleanly split (the file carries only that declared section, nothing else mixed in), can be re-vendored directly: byte-matching it against the hub canonical is the point of `verbatim` fidelity, and the audit already checks it that way. The guard above is for everything else: `intent`-fidelity content, a file that has not been split yet, or any file you are not certain is clean. + +## If you are not sure which case you are in + +Stop and say so, rather than guessing. Naming the uncertainty costs one sentence. Silently overwriting the wrong thing costs someone's local rules with no way to notice until much later. diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md new file mode 100644 index 00000000..af775184 --- /dev/null +++ b/.github/skills/code-review/SKILL.md @@ -0,0 +1,71 @@ +--- +name: code-review +description: >- + Reviews a pull request or change set against the repository's contracts, with explicit diff + coverage and no suppressed findings. Use this whenever asked to review code, a pull request, + a patch, or a proposed change, and whenever GitHub Copilot performs code review. Triggers even + when the diff is documentation-only or workflow-only, because the review must load the + applicable general, language, documentation, and workflow skills before judging the change. +--- + +# Code Review + +## Establish the Contract + +1. Read the root `AGENTS.md` and the sections it routes to for the changed paths. +2. Read the complete diff and enumerate every changed file before forming findings. +3. Load every applicable sibling skill from the current skill distribution: + - `comment-and-doc-style` for Markdown, prose, comments, commit messages, and PR titles. + - `dotnet-codestyle` for C# and .NET changes. + - `python-codestyle` for Python changes. + - `shell-codestyle` for shell changes. + - `workflow-ci-contract` for GitHub Actions and CI/CD changes. +4. Treat a missing executable on `PATH` as no evidence that its check is unavailable. Read the + repository's documented local invocation before reporting a check as skipped. + +Do not substitute a familiar convention for the repository's written contract. Report a +conflict between instructions instead of silently choosing one. + +## Review the Change + +Review for correctness, regressions, security, compatibility, error handling, concurrency, +resource lifetime, tests, and contract drift. Follow data and control flow beyond the edited +lines when the behavior depends on unchanged callers or consumers. + +For each candidate finding: + +1. Verify it against the current head tree, not an unfetched checkout or the base branch. +2. Identify the concrete failing behavior and the conditions that reach it. +3. Confirm that the repository does not already prevent it elsewhere. +4. Prefer one root-cause finding over several symptoms of the same defect. +5. Omit pure preferences that no repository rule or user-visible risk supports. + +Review carried fleet content by intent and fidelity. A byte-locked reference to a path that one +downstream repository does not carry is not a broken link. A substantive defect in canonical +content remains a finding, with the fix located at its canonical source. + +## Publish Every Finding + +Never suppress or hide a finding because confidence is low. Investigate until it is supported +or discard it. Publish every supported finding as an inline review comment when a changed line +can anchor it. Use the review body only when no valid inline anchor exists. + +Each finding states: + +- A concise imperative title with a severity. +- The file and smallest useful line range. +- The behavior that fails and the input or state that triggers it. +- Why the change causes the failure. +- A bounded direction for the fix when one is known. + +Do not report a clean review until every changed file has been read. End the review body with +exactly one ASCII marker, replacing the numbers with measured counts: + +```text + +``` + +`reviewed` is the number of changed files actually reviewed. `changed` is the total number of +changed files. `findings` is the number of published findings, including body-only findings. +Never emit `reviewed=changed` as a placeholder. If full coverage is impossible, emit the actual +counts and explain the limitation in the review body. diff --git a/.github/skills/comment-and-doc-style/SKILL.md b/.github/skills/comment-and-doc-style/SKILL.md new file mode 100644 index 00000000..af574e92 --- /dev/null +++ b/.github/skills/comment-and-doc-style/SKILL.md @@ -0,0 +1,251 @@ +--- +name: comment-and-doc-style +description: >- + Governs prose, comment, Markdown, character-set, line-ending, and PR-title/commit-message + conventions for every ptr727/ProjectTemplate fleet repo. Use this whenever writing or editing a + code comment, workflow comment, Markdown doc, commit message, or PR title, whenever choosing + which characters to type in agent-authored text, whenever the file being edited is CRLF, and + whenever naming a tool in prose or docs. Triggers even when the task looks purely mechanical, + such as "just fix a typo" or "add a one-line comment", because the fleet's ASCII character-set + tiers, no-semicolon rule, comment-growth discipline, and CRLF-preservation rule are each easy to + violate without noticing: an em dash slipped into a sentence, a comment that grew by one more + clause, or a text-mode edit that silently flattens a CRLF file to LF. Also triggers when + authoring a new Markdown file (reference-style links, Table of Contents, present tense), when a + carried instruction file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, + .github/copilot-instructions.md) is being edited (no coordination references to the template or + a sibling repo), and when writing a PR title or commit message (imperative subject, no vague + titles, no unsolicited Co-Authored-By, no release-bump magnitude). +--- + +# Comment and Doc Style + +## Why this exists + +These are the fleet's mechanical prose rules, kept in one place instead of re-derived per repo or +per session: how to write a comment, which characters an agent may type, how a Markdown file is +structured, how a carried instruction file may reference the hub, and how a PR title or commit +message reads. None of these are matters of taste. Each is checked, by `prose_lint.py`, +`editorconfig-checker`, `markdownlint`, `cspell`, or a human reviewer, and each has been the exact +subject of a real review finding. + +## Naming tools in prose + +Use each tool's official casing in task labels, docs, and prose: `.NET` (not `.Net`), +`CSharpier`, `ruff`, `pyright`, `uv`. Do not invent personal variants. + +## Markdown files: linting and spelling + +- **Markdown lints clean, repo-wide.** Every `.md` file is error and warning free via + `markdownlint-cli2` against the shared `.markdownlint-cli2.jsonc`. A rule it deliberately + disables (for example `MD013` line length) stays disabled, do not "fix" it. `MD033` inline HTML + stays enabled: HTML comments, and `details`/`summary` (no Markdown equivalent for a + collapsible), are allowed, everything else with a native Markdown equivalent uses the Markdown. +- **Spelling is US English**, checked by CSpell against the shared `cspell.json` + (`"language": "en-US"`, so a British spelling is flagged). Add a project term to `cspell.json`'s + `words` list, never to a `.code-workspace`'s own `cspell.words` block. +- **CI's spelling gate covers `README.md` and `HISTORY.md` only**, deliberately not every `.md` + file, so a new topical doc is not spell-gated in CI (the editor extension still flags it live). + A repo may widen its own CI list, README plus HISTORY is the default. A repo shipping no + `HISTORY.md` drops it from the CI workflow, the `Lint: Spelling` task, and the GOVERNANCE.md + cspell line together, all three or none. +- **`HISTORY.md` mirrors the README's opening**: the same `# `, the same tagline verbatim + (the first line after the README's H1), then its own `## Release History`. It never repeats a + paragraph below the README's tagline. +- **"Markdown" is a proper noun in prose** (a Markdown file, a Markdown-only repo), lowercase only + for what a machine reads: a tool or package name (`markdownlint`), a settings key, a heading + anchor, a file extension. + +## Docker lint authorization + +A restricted executor treats Docker socket access, image fetching, and repository exposure as +separate permissions. Repository exposure needs explicit maintainer approval even when the mount +is read-only. Use the hub's `scripts/docker_lint.py` wrapper for the standard lint shape. It +discovers targets, pulls images in a separate phase, resolves each digest, and announces the +boundary before repository mounts begin. Each Docker command has a timeout and visible result. +Lint containers disable networking and mount the checkout read-only. Persist approval only when +the executor constrains that whole shape. Never allow an unconstrained `docker run` prefix. +PSScriptAnalyzer downloads its pinned module in a separate container that has network access and +no repository mount. `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)" +owns the exact invocation and full authorization model. + +Agent-specific authorization stays in provider-labeled bullets so one agent's configuration does +not read as a shared requirement: + +- **Codex:** rules cannot safely cover changing worktree paths and digests. Smart Approvals can + prompt per task. No-prompt operation is supported only inside an external sandbox because it + removes command-wide protection. + +## Markdown formatting + +- **Reference-style links everywhere**, except the four files read one section at a time rather + than end to end: `AGENTS.md`, `GOVERNANCE.md`, `OPERATIONS.md`, `.github/copilot-instructions.md`. + Those keep inline links so a target resolves where it is read. Every other Markdown file defines + every URI at the bottom, grouped by type under an HTML-comment header, each group alphabetized + by reference name rather than by the full definition line (a name that is a prefix of another + sorts first, `[governance]` above `[governance-branching-model]`). A URL inside a fenced code + block stays inline. See `references/markdown-links.md` for the full grouping and naming + convention. +- **Table of Contents**: generated by the Markdown All in One extension on save, never + hand-authored or hand-edited. Exclude a heading with an inline `<!-- omit from toc -->` marker. +- **One logical paragraph per line**, no hard-wrap line-length limit. For an intentional line + break within a block (stacked badges, status lines), end the line with a trailing backslash + rather than trailing whitespace. +- **Headings use the PR-title casing rule** below. +- **Write in the present tense.** State what *is*, never a change from a prior state ("X does Y", + not "X now does Y" or "X no longer does Z"). This applies to docs and code/workflow comments + alike. Before/after framing belongs in changelogs, commit messages, and PR descriptions, where + the prior state is the point. +- **When a behavior changes, grep for prose asserting the old one.** Comments, diagram labels, + workflow-input descriptions, and audit statements elsewhere may still describe the prior + behavior, and each was accurate when written. No linter catches a claim that is merely untrue, + so this sweep is the only mechanism that will. + +## Sentence structure + +The structural half of ASD-STE100 is the adopted house style for agent-authored prose, and the +controlled dictionary is deliberately not adopted: vocabulary stays unrestricted, structure is +restricted. Each structural rule a pattern can reach lands as a `prose_lint.py` check +incrementally, and this section names each check as it ships. + +- **Short sentences: at most 25 words in one sentence**, ASD-STE100's descriptive cap, checked by + the `sentence-length` rule in `prose_lint.py`. The check is opt-in like `sentence-split`, + because the existing corpus predates the cap and a default gate would fail whole files nobody + is editing. Write new prose under the cap, and scope a run to a change with + `--check sentence-length --diff <base>`. +- **One instruction per sentence.** A procedure step states one action, and a second action is a + second step. No pattern reaches this, so it is authoring discipline with no check. +- **Active voice, imperative mood for procedure steps.** Write "run the gate", never "the gate + should be run". Also authoring discipline, since a reliable passive-voice pattern does not + exist. + +## Comments + +Applies to code and workflow (`#`) comments alike. + +- Comment only when the code does not explain itself, or the logic is genuinely complex. + Self-evident code needs no comment. +- State only the non-obvious *why*, for the human reading *this* project's code now. No + cross-project references, no historic or design narrative, no rule citations. Governance lives + in the fleet's own instruction set, not echoed inline. +- **Keep it short**: one line is the default. A second line is earned only by a constraint the + code cannot otherwise carry. +- **Structured, not prose**: one sentence per line, never wrapped across lines, never a + multi-sentence run-on. A comment that genuinely needs several sentences is several lines, each + one sentence. +- A comment line opening prose starts with a capital. A trailing label, or the version pin an + action-pinning rule requires, does not. +- Mark a sub-topic with `-` after the comment marker (`# -`), only for genuine parallel sub-items + hanging off a lead line, never a continuation of one thought. +- **No file, class, or type header summary blocks.** A type or file gets a comment only for a + specific non-obvious point, never a block restating what it contains (a license or provenance + header a tool or policy requires is not a summary and is unaffected). +- **Never let a comment grow across edits.** Touching code near an existing comment means the + comment comes out the same length or shorter, never one more clause of rationale appended. + +A continuation stays unindented, one sentence per line: + +```text +# Change gate for the compile tests. +# An esp-idf build costs minutes, so gate on what each test covers. +# A diff that cannot be computed runs everything. +``` + +Sub-topics take a `-` after the comment marker, each elaborating a distinct item named in the lead: + +```text +# Source lint plus change-gated compile tests. +# - compile-test builds the external component. +# - template-compile-test builds one example device per template. +``` + +## Character set + +Agent-authored text is ASCII by default: documentation, code, comments, commit messages, and PR +descriptions. A non-ASCII character is read against three tiers, because whether one is +typography or meaning depends on where it sits. A character in no tier is a finding rather than a +silent pass. + +- **Tier 1, never legitimate.** Typography carrying no meaning its ASCII form loses. Remove on + sight: + - em dash (U+2014) and en dash (U+2013) to a restructured sentence, two sentences or a comma, + never a spaced hyphen + - right arrow (U+2192) to `->`, double arrow (U+21D2) to `=>` + - curly quotes (U+2018/U+2019/U+201C/U+201D) to straight `'` and `"` + - ellipsis (U+2026) to `...`, bullet (U+2022) to `-` + - no-break space (U+00A0) to a space, non-breaking hyphen (U+2011) to `-` +- **Tier 2, legitimate only next to a number.** Relational and arithmetic operators: U+2264, + U+2265, U+2260, U+00B1, U+2212, U+00D7, U+00F7, U+00B7. Keep one when an adjacent non-space token + is a number, a tier-3 symbol, or another tier-2 operator, so a threshold table or a measured + range reads as the range it is. In flowing prose write the ASCII form: `<=`, `>=`, `!=`, `+/-`, + `-`, `x`, `/`. A tier-2 operator directly before a number in a table of thresholds is the range + it describes and stays, the same character between two words in a sentence is prose and takes + the ASCII form. +- **Tier 3, always legitimate.** Scientific and unit symbols whose ASCII form would be a lie: + micro (U+00B5), degree (U+00B0), ohm (U+2126), pi (U+03C0), superscript two and three (U+00B2, + U+00B3), section (U+00A7). Keep the symbol, never approximate it away or spell it out. +- **Unicode a developer deliberately typed** stays regardless of tier, such as emoji used for + emphasis or as callout markers. Never strip a developer's own characters, this is developer + authored text and not a license for the agent to add its own. +- **An unrecognized non-ASCII character is reported, not allowed.** Classify it into a tier above + before using it. +- **No semicolon in agent-authored prose.** Recast a mid-sentence semicolon as a comma or as two + sentences. A semicolon separating items in a list that already contains commas, or a statement + terminator in code, is unaffected. +- **No spaced hyphen joining or interrupting a sentence** (` - `, or the paired aside ` - x - `). + Recast as a comma, two sentences, or parentheses. A hyphen inside a compound word, a leading + list marker, a range, and the `- **Label** - explanation` bullet separator are unaffected. +- **In carried verbatim content, fix the whole class at the hub**, not one instance, since a + downstream repo cannot edit a section byte-matched against the hub. Everywhere else, correct as + each file is next edited, not swept. + +## Line endings + +This repo's default is LF (`[*] end_of_line = lf` in `.editorconfig`), with CRLF pinned only for +`*.bat` and `*.cmd`, the one type Windows itself requires it for. +**Preserve a file's existing line ending when editing it, never reflow as a side effect of a +content change.** A text-mode tool, including a naive programmatic write, can silently flip CRLF +to LF and turn a one-line change into a whole-file diff. After any programmatic edit, verify with +`git diff --stat` (it should touch only the lines you changed) and a byte scan, `file` and a naive +`git ls-files --eol` are both unreliable here. Idempotent normalize: +`b.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")`. The full policy, choosing an ending for a new +file type, operational-repo overrides, extensionless-script pins, and auditing, is in +`references/line-endings.md`. + +## Carried files reference no coordination machinery + +`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, +the `spec/` files and the carried `AUDIT.md` never reference the template repo +(in prose or a link), and never name a sibling fleet repo as an illustrative example. State the +behavior a carried rule needs, not the coordination flow that produced it, the maintainer supplies +the destination out of band. A contextually relevant link to a related project (the image this +config feeds, a library this depends on) is not a coordination reference and is expected. The full +exceptions, a verbatim section that must name the hub to do its job, and a pointer to a +hub-hosted tool the reader runs, are in `references/carried-doc-references.md`. + +## PR titles and commit messages + +- **Format**: an imperative subject, 72 characters or fewer, no trailing period ("Add 24-Hour + PM2.5 Average Sensor", not "Added X" or "Adds X"). An optional body, blank-line separated, + explains *why* the change is being made when that is non-obvious, the diff already shows *what*. +- **Rules**: no vague titles (`update stuff`, `wip`). Dependabot's default `Bump X from Y to Z` + titles are fine as-is. No `Co-Authored-By:` lines unless the developer explicitly asks. No + release-bump magnitude in the title ("minor", "patch", "release v0.2.0"), Nerdbank.GitVersioning + computes the next version from `version.json` and git history, a dependency version in a + dependency-bump title is fine and expected. US English spelling, and title case with lowercase + short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from), a hyphenated + compound capitalizes both parts unless the second is a short preposition (*Built-in*, + *EPA-Corrected*, *24-Hour*). + +```text +Add Structured Logging Extensions to Library +Pin softprops/action-gh-release to Commit SHA +Drop net8.0 Multi-Targeting from Console Project +Bump xunit.v3 from 3.2.2 to 3.3.0 +Clarify devcontainer Setup Steps in README +``` + +## Quantitative claims + +A quantitative claim in `README.md` (a count, a size, a version floor, a supported-platform list) +is verified against current code before it is written. When a doc number is derived from a code +constant, mark the dependency in a source-code comment so the next editor knows to update both. diff --git a/.github/skills/comment-and-doc-style/references/carried-doc-references.md b/.github/skills/comment-and-doc-style/references/carried-doc-references.md new file mode 100644 index 00000000..125bf0f8 --- /dev/null +++ b/.github/skills/comment-and-doc-style/references/carried-doc-references.md @@ -0,0 +1,61 @@ +# Carried Files Carry No Coordination References + +Full detail for the "Carried files reference no coordination machinery" rule in `SKILL.md`. Load +this when editing one of the carried files themselves, not when writing an ordinary repo-owned +doc. + +## Which files this governs + +`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, +the `spec/` files and the carried `AUDIT.md`, the files the fleet carries +verbatim or at `intent` fidelity from the hub into every repo. This rule governs carried template +content only. A repo's own `README.md` and topical docs are its own content, never carried +verbatim, and this rule does not reach them. + +## What is banned + +Two things, in the files above: + +1. **Any reference to the template repo**, in prose or in a link. The coordination flow that + produced a carried file is machinery a consumer of that repo should never have to see, and + naming where a file came from is exactly the derived-from framing the present-tense rule (in + `SKILL.md`'s "Markdown formatting" section) independently forbids. Where a carried file must + express a template-level behavior ("report a rule discrepancy upstream"), state the behavior + rather than the destination. The maintainer supplies the destination out of band. +2. **A sibling fleet repo named as an illustrative example** ("repo X does it this way", "see repo + Y's adoption"), which couples the repos and rots as they diverge. To point at a current good + example, name it in the onboarding or conformance issue, never in a carried doc. + +## The two exceptions + +**The first exception is a verbatim section**, and `AGENTS.md` "Fleet Bootstrap" is why it exists. +That section's whole function is to name where the canonical rules live, for an agent in a +repository whose carried copies are stale, partial, or absent, which is exactly when no other file +present can say it. Its bytes are fixed fleet-wide, so a repository cannot edit the reference out +without failing the verbatim check instead, and a rule banning it would be unsatisfiable rather +than merely strict. The exception is scoped to the verbatim region and never leaks past it: the +same document's own prose, outside that region, is governed normally. A reference that reaches a +verbatim section is a defect in the canonical, fixed once at the source rather than reported +against every repository carrying it. + +**The second exception is a hub-hosted tool the reader is told to run**, which is a different kind +of reference. A rule naming a gate, a script, or a reference snippet the reader executes or copies +states an instruction rather than a provenance, and an instruction with no destination is +unfollowable, which is precisely how a pointer in carried text comes to read as decorative. The +test is whether the reference is something the reader *does* or something that *happened to this +file*: where the content came from stays out, what the reader runs stays in. Such a pointer names +the hub's canonical rather than this repository's provenance, so it is the hub's to keep resolving +and never a repository's to edit out or re-point at a local path. What is reached rather than +carried, and how, is `GOVERNANCE.md` "Hub-Hosted Tooling". In `AGENTS.md` and `GOVERNANCE.md` this +belongs in verbatim rule text, the same region the first exception already covers, so the whole +fleet reads one wording and no repository is asked to answer for a reference it did not write. + +## What is not a coordination reference + +**A contextually relevant link to a related project is expected, not banned.** Where another repo +is part of this repo's subject matter (the image that consumes this config, the builder that +generates this hardware, a library this depends on), link it normally. The test is whether the +link serves a reader of *this* repo's content, not whether the target happens to be in the fleet. + +This pairs with the present-tense rule: state the current shape, not a history of which repo it +came from. diff --git a/.github/skills/comment-and-doc-style/references/line-endings.md b/.github/skills/comment-and-doc-style/references/line-endings.md new file mode 100644 index 00000000..8337cbcf --- /dev/null +++ b/.github/skills/comment-and-doc-style/references/line-endings.md @@ -0,0 +1,117 @@ +# Line Ending Policy + +Full detail for the "Line endings" rule in `SKILL.md`. Load this when choosing an ending for a +new file type, working in an operational (config) repo, pinning an extensionless executable, or +auditing a repo's endings, not for an ordinary content edit to an existing file (the SKILL.md +summary, preserve the existing ending and verify with a byte scan, covers that case). + +## The defaults + +- **`.editorconfig` sets the line ending.** `[*] end_of_line = lf` is the default, every file + type is LF unless pinned otherwise, with CRLF pinned for the one exception Windows requires: + `*.bat` and `*.cmd` (cmd.exe's line handling is unreliable on LF). Only the CRLF exception is + declared, the redundant per-type LF rules are intentionally omitted, since the default already + gives shell scripts, Dockerfiles, workflow YAML, `uv.lock`, and every shebang-executed `.py` + the ending they need without a path-specific pin. +- **`.gitattributes` mirrors the repository-wide defaults**: `* text=auto eol=lf` normalizes every + detected text file to LF while leaving binary files byte-preserved. `*.bat` and `*.cmd` override + that default to CRLF. Do not add per-language or per-file LF pins where the global LF default + already applies. The CRLF-native exception for POSIX-executed paths is defined below. +- **Both files are required together.** `.editorconfig` governs the editor, `.gitattributes` + governs git (checkout, commit, `--renormalize`). A repo missing either file, or whose + `.editorconfig` sets no global `end_of_line` default (for example declares it only under + `[*.md]`), accumulates files mixed between LF and CRLF, the exact failure these two files + prevent together. Carry both files whole. An inert `[*.cs]` block costs nothing in a non-.NET + repo. + +## Choosing an ending for a new file type + +LF is the default, since it is what every tool, CI runner, and Dependabot bump produces, and +Windows GUI editors (VS Code, Visual Studio, Notepad, WordPad) all read and write it cleanly. Pin +CRLF only for a type Windows itself requires it for: `*.bat` and `*.cmd`. Everything else, +including YAML (workflow and non-workflow alike, no distinction needed now that both are LF), +`.gitignore`, `.dockerignore`, and a tool-owned format with a native LF ending (KiCad), takes the +`[*]` default with no override. + +## Operational (config) repos + +The global default follows the consuming application's native platform, not the fleet LF default. +A config repo (registry `workflowModel: operational`) is a view into an application's +configuration directory, often the exact tree mounted into that app's container, so its files use +the ending the app itself reads and writes, and forcing the fleet LF default would fight an app +that needs CRLF. Set the `[*] end_of_line` default to the app's native ending and record it in the +registry `lineEndings` field (`lf` or `crlf`): the field is required for every operational repo +because a config repo's ending is a load-bearing decision tied to its consuming app. A +Linux-native app or container config uses the `lf` fleet default. A Windows-native app that uses +CRLF requires `[*] end_of_line = crlf` and +`* text=auto eol=crlf`. `release` repos keep the LF fleet defaults above. Do not re-normalize an +operational repo to +the fleet default, that is exactly the over-normalization these per-repo endings exist to +prevent, whichever direction the fleet default currently points. + +**Mixed-consumer config: prefer to split by platform into single-platform repos, not one mixed +repo.** When a config repo would be consumed on two platforms (a Linux app plus a Windows-edited +subtree), the clean answer is a repo per consumer, each single-platform with its own +`lineEndings`. For example a controller config edited by a Windows-native editor lives in its own +CRLF repo, not as a subtree inside a Linux `lf` config repo. Fallback only if a subtree genuinely +cannot be split out: keep the global default at the primary consumer and pin the odd subtree with +an `.editorconfig` path override (for example `[<subtree>/**] end_of_line = crlf`) matching its +consumer. Pair the same path override in `.gitattributes`, since both layers must resolve the +path to the same ending. + +## Scripts and extensionless executables + +Must be LF. A CRLF shebang (`#!/usr/bin/env bash\r`) breaks execution. The paired global LF +defaults cover extensionless executables, shell scripts, and directly executed Python without +path-specific pins. A CRLF-native operational repo adds narrow matching LF overrides in both +files only for scripts it executes on POSIX. + +For a type that genuinely needs an ending the `[*]` default no longer supplies (a Windows-native +tool-owned format outside `.bat`/`.cmd`, or a byte-preserve data directory whose exact bytes the +consumer may depend on), still pair a `.gitattributes` pin with a matching `.editorconfig` +override, since the git pin alone is not enough there, `.gitattributes` governs git while the +editor follows `.editorconfig`. For a byte-preserve directory, disable all editor normalization, +not just EOL: `[<dir>/**]` with `charset = unset`, `end_of_line = unset`, `insert_final_newline = +false`, `trim_trailing_whitespace = false` (`unset` is EditorConfig's spec-defined special value +that removes an inherited property, and `**` is needed rather than `*` so a nested file under the +directory is covered too, since `*` excludes `/` and only matches one path component). + +## Editing discipline + +- **New files**: create with the `.editorconfig`-mandated ending. +- **Editing an existing file**: preserve its current line endings, do not reflow them as a side + effect of a content change, even if the file is already non-compliant. A tool that rewrites a + file in text mode (a script, a bulk find/replace) can silently flip CRLF to LF and turn a + one-line change into a whole-file diff. After any programmatic edit, verify before staging: + `git diff --stat` should touch only the lines you changed, and a byte check should confirm the + expected ending. If a diff balloons to the whole file, the endings flipped, restore them and + re-stage. +- **Fixing a non-compliant file**: bring it to its `.editorconfig` ending as a deliberate change, + and prefer to isolate it in its own EOL-only commit so the churn is reviewable. When a broader + maintenance change has to normalize endings alongside content edits, call it out explicitly in + the commit or PR description and verify the content separately with + `git diff --ignore-cr-at-eol`. + +## Auditing + +Don't trust `file` or a naive `git ls-files --eol`. The authoritative check is a byte scan that +classifies by which endings are present: CRLF-only (every `\n` preceded by `\r`), LF-only (no +`\r`), or mixed (both forms present). Flag mixed explicitly rather than lumping it in with CRLF, +and skip binaries via a NUL-byte check. `file` mislabels some types (it reports a CRLF `.json` or +`.code-workspace` as plain "JSON text data" with no CRLF note), and `git ls-files --eol`'s `attr/` +column holds multiple tokens that shift naive field-splitting into false positives. Scope a +repo-wide audit to `git ls-files` plus `git ls-files --others --exclude-standard`, never a raw +`find`, which sweeps self-ignoring caches (`.mypy_cache`, `.artifacts`). + +Idempotent normalize: `b.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")`. A single within-line +string replace is EOL-safe, but a tool that inserts multiple lines or writes a new file into a +CRLF file must emit `\r\n`, since a naive `\n` insert creates mixed endings. `.code-workspace` is +JSONC (it has `//` comments), so strip them before JSON-parsing it. + +Editing CRLF files programmatically with a regex has a sharper trap: `.` matches `\r`, so a +captured line keeps its carriage return and rejoining with `\r\n` yields `CRCRLF`. A text-mode +rewrite has the mirror failure, silently flattening CRLF to LF. Prefer line-based edits +(`splitlines(keepends=True)`) or literal replacement over regex reassembly. In Python the +text-mode failure is the default: `Path.read_text()` decodes through universal newlines and +`write_text()` writes `\n` back, so a read-edit-write round trip flattens the whole file while the +edit itself looks correct. Pass `newline=''` to both, or work in bytes. diff --git a/.github/skills/comment-and-doc-style/references/markdown-links.md b/.github/skills/comment-and-doc-style/references/markdown-links.md new file mode 100644 index 00000000..c3cd7e1a --- /dev/null +++ b/.github/skills/comment-and-doc-style/references/markdown-links.md @@ -0,0 +1,64 @@ +# Reference-Style Links + +Full detail for the "Markdown formatting" reference-style-links rule in `SKILL.md`. Load this +when actually authoring or reorganizing a Markdown file's link definitions, not for a small +in-place prose edit. + +## Where the rule applies + +Every Markdown file in the repo uses reference-style links only, except the four files that are +read one section at a time rather than end to end: `AGENTS.md`, `GOVERNANCE.md`, `OPERATIONS.md`, +and `.github/copilot-instructions.md`. Those keep inline `[text](uri)` links, since a reader +jumping straight to one section needs the target to resolve where it is, while a definition parked +at the bottom of the file is never reached. The exception is that closed list of four files, never +a category to argue from case by case. Every other Markdown file follows the rule regardless of +its audience. + +## The definition block + +Every URI, an internal path, an anchor, an external URL, or a shield image, is defined at the +bottom of the file, split into groups by type under an HTML-comment header, for example: + +```markdown +<!-- Shields --> + +[license-shield]: https://img.shields.io/... + +<!-- Repo --> + +[governance]: ./GOVERNANCE.md +[governance-branching-model]: ./GOVERNANCE.md#branching-model + +<!-- External --> + +[markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2 +``` + +Within a group, definitions are alphabetized by **reference name alone**, the text inside the +brackets, never by the whole definition line. Where one name is a prefix of another, the shorter +one sorts first: `[governance]` above `[governance-branching-model]`, `[repo-config]` above +`[repo-config-settings]`. Sorting the full line instead inverts every such pair, because `-` +precedes `]` in byte order, so the two readings disagree on exactly the names a reader looks up +together, and a plain `sort -c` over the block passes on the inverted order regardless. + +## Naming a reference + +Reference names are contextual and encode both the target and its group: + +- `foo-shield` for a shield image +- `foo-link` for an external URL +- a bare `foo` for a local path or anchor + +For example `[license-shield]`, `[releases-link]`, `[repo-config]`. Never a numeric name (`[1]`) +and never an opaque one. + +## Mechanics + +- No inline `[text](uri)` targets in prose, in any file outside the four-file exception above. +- **A URL inside a fenced code block stays inline.** Reference links do not resolve inside a code + block, so do not extract it there, and exclude fenced code from any link-integrity check + (bracket literals like `["a", "b"]` otherwise read as undefined references). +- **Removing a link also removes its reference definition.** An orphaned definition fails the + no-unused-defs rule. +- The one exception to "no inline links" is the Table of Contents, whose entries stay inline + anchor links, since the ToC extension generates them that way and they are never hand-edited. diff --git a/.github/skills/copilot-instructions-keeper/SKILL.md b/.github/skills/copilot-instructions-keeper/SKILL.md new file mode 100644 index 00000000..9ee43bcc --- /dev/null +++ b/.github/skills/copilot-instructions-keeper/SKILL.md @@ -0,0 +1,95 @@ +--- +name: copilot-instructions-keeper +description: >- + Helps keep a repo's .github/copilot-instructions.md in sync with the ptr727/ProjectTemplate hub + canonical, and stops the one mistake specific to this file: silently wiping its repo-local + "Disproved Claims" ledger entries during a resync. Use this whenever about to edit, overwrite, + re-vendor, or carry .github/copilot-instructions.md into a repo, whenever checking a repo for + drift against the hub or running a conformance sweep that touches this file, whenever GitHub + Copilot's review mechanics in this file look stale, wrong, or missing something the fleet + runbook should cover, or whenever standing up a new repo and carrying this file for the first + time. Also triggers on "why isn't the audit catching that this file is out of date," since the + fleet's mechanical audit checks this file, at intent fidelity, for file presence and each named + section's heading, never for content drift inside a section, so nothing else notices a stale + section here except a live check like this one. +--- + +# Copilot Instructions Keeper + +## Why this exists + +`.github/copilot-instructions.md` is read directly by GitHub Copilot and bootstraps the shared +`AGENTS.md` instruction set and review-focused skills. Its Copilot-specific rules stay fully +intact in every repo that carries it. This skill maintains that carried copy, it does not replace +the bootstrap. + +`spec/files.json` declares it `intent` fidelity, `whole: true`, covering four named sections +(`Commit Messages and Pull Request Titles`, `Reviewing Carried Fleet Content`, `GitHub Copilot +Review Runbook`, `When in Doubt`), with `<owner>`, `<repo>`, and `<N>` placeholders filled per +repo. **The fleet audit checks an `intent` file for file presence and each named section's +heading, never for content drift inside a section.** A section that is present but has fallen out +of date against the hub, the exact gap this skill exists to catch, produces no finding anywhere in +the mechanical audit. Noticing that has to happen in a live session like this one. + +## The one thing this file has that others don't: repo-local ledger entries + +The file's own "Disproved Claims" section states its rule plainly. **The section's shape and +governing rules are carried, but its entries are not.** Each entry records a finding that was +raised against this specific repository and disproved against this repository's code at a named +revision. A repository carrying a copy of this file carries the shape and rules, deletes any +entry whose subject it does not hold, and records what it has proved for itself. + +This means a blind re-vendor of the hub's canonical `.github/copilot-instructions.md` over a downstream +repo's copy is wrong in both directions: + +- Copying the hub's own "Disproved Claims" entries (about `ProjectTemplate` itself) into a + downstream repo attaches proofs about code that repo does not carry. +- Overwriting a downstream repo's copy wholesale deletes any entries that repo itself has earned, + a live disproof, run against that repo's own tree, thrown away with no record. + +**Before touching this file in any repo other than the hub itself:** + +1. Read the current "Disproved Claims" section in that repo's copy, if it has one, and preserve + every entry that names a file or behavior that repo actually carries. +2. Update everything else, the runbook mechanics, the four named sections, the rule text, to + match the hub canonical. +3. Never carry the hub's own repo-specific "Disproved Claims" entries downstream. They name + `ProjectTemplate`'s own files and revisions, not the target repo's. +4. If in doubt whether an entry is still valid for the current tree, treat it per the guard skill + below rather than guessing. + +The `carried-instruction-file-guard` skill stops this same failure class for `AGENTS.md`, +`GOVERNANCE.md`, `CODESTYLE.md`, and `WORKFLOW.md`: a +routine-sounding overwrite silently deleting content that is not a stale copy of the hub. Run +that skill's distinctive-phrase probe against this file too before any full-file replace. It is +not in that skill's own file list because its failure mode, ledger entries rather than fleet +rules, is specific enough to warrant its own skill, but the underlying discipline, probe before +overwrite, give a local addition a destination rather than deleting it, is the same. + +## Checking a repo's copy for drift + +1. Fetch the hub (`github.com/ptr727/ProjectTemplate`) `main` branch fresh. A stale local clone + answers confidently instead of failing. +2. Compare the target repo's `.github/copilot-instructions.md` against the hub's, section by + section, at **intent** fidelity, judged by meaning, not by byte match. A content-identical + file with different `<owner>`/`<repo>` placeholder fills is current, not drifted. +3. Read the "Disproved Claims" section separately from the rest. Judge its **shape and rules** + against the hub, and judge its **entries** only against what that repo itself carries (see + above), never against the hub's own entries. +4. Report what is actually stale (a runbook mechanic that changed, a rule that moved, a new + section) versus what only looks different because it is correctly repo-specific. + +## Carrying it fresh, new repo or full resync + +Follow `RESYNC.md`'s general apply order for carried files, with the ledger rule above applied at +the point this file is touched: carry the hub's current rule text and runbook mechanics, keep the +target repo's own "Disproved Claims" entries (if any existed pre-resync) rather than replacing +them with the hub's, and start a new repo's ledger empty rather than seeded from the hub's own +proofs. + +## What this skill does not cover + +Content-style rules for other carried files (`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, +`WORKFLOW.md`) are `carried-instruction-file-guard`'s job. The review-loop contract this file's +runbook implements, the merge gate, triage, escalation, is `pr-review-conduct`'s job. This skill +is narrowly about keeping this one file's carried copy correct. diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md new file mode 100644 index 00000000..f3565a5c --- /dev/null +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -0,0 +1,211 @@ +--- +name: dotnet-codestyle +description: >- + Governs C#/.NET code style for ptr727/ProjectTemplate fleet repos: the zero-warnings build + policy and its three-task clean-compile chain, central Directory.Build.props/ + Directory.Packages.props configuration, C# language and naming conventions, XML documentation, + analyzer suppression scope, the library-versus-application logging split, async and + error-handling patterns, xUnit v3 + AwesomeAssertions testing conventions, and AOT-compatible + project configuration. Use this whenever writing, reviewing, or editing a .cs file, a .csproj, + Directory.Build.props, or Directory.Packages.props, whenever choosing where to suppress an + analyzer diagnostic, whenever a NuGet library needs to log without depending on Serilog + directly, or whenever writing or reviewing an xUnit test. Triggers even when the task looks + like a small local fix ("just silence this warning", "add a quick log line", "bump a package + version"), because the zero-warnings policy, the suppression-scope order, the central-package- + management rule, and the library/application logging split are each easy to violate one file at + a time without the pattern ever showing up as a single obvious diff. Applies only to a repo's + .NET side, a repo with no .NET projects has no use for this Skill. +--- + +# .NET Codestyle + +## Why this exists + +This is the .NET-specific half of the fleet's code style guide, kept in one place instead of +re-derived per repo or per session. CODESTYLE.md's General section still owns the rules every +language shares (clean-compile verification as a concept, the suppression-scope order, tooling +casing in prose), this Skill is everything specific to a C#/.NET project on top of that: the +concrete `.NET Format` task chain, the analyzer configuration that makes the zero-warnings policy +real, and the language, naming, logging, and testing conventions. + +## Build requirements + +### Zero warnings policy + +All builds must complete without warnings, enforced three ways: + +- **The `.NET Format` clean-compile task.** It chains `CSharpier Format` -> `.NET Build` -> + `dotnet format style --verify-no-changes`. A repo carries those three task definitions in its + own `.vscode/tasks.json`, matching the canonical `vscode-tasks.json` snippet at + `github.com/ptr727/ProjectTemplate/blob/main/catalog/snippets/configs/vscode-tasks.json`. Run + the `.NET Format` task after any code change, before commit. To run it natively instead, + reproduce that exact task chain (`CSharpier Format`, then `.NET Build`, then + `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`) without dropping + or loosening any argument, reading it from that same canonical snippet. Bare `dotnet format` + alone, skipping CSharpier or the build, is not sufficient. +- **Analyzer configuration.** `<EnableNETAnalyzers>true</EnableNETAnalyzers>` with + `<AnalysisLevel>latest-all</AnalysisLevel>` and `<AnalysisMode>All</AnalysisMode>` (the full + analyzer set), plus `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>`, so any diagnostic + surfaced as a warning fails the build and must be fixed or deliberately suppressed at the + narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. +- **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. + A working local hook is strongly suggested, not optional: wire Husky.Net from the canonical + `catalog/snippets/husky/` config. See GOVERNANCE.md "Running the Linters Locally" for what the + hook must cover and what its absence means. + +**A new port is not a license to silence diagnostics.** Brownfield or just-ported status never +justifies relaxing analyzer severities or muting newly surfaced warnings. Fix them. (The only +brownfield allowance in the fleet is the one-time git-signing / line-ending migration described in +GOVERNANCE.md and README.md, which has nothing to do with code analysis.) + +### Central build and package configuration + +Shared MSBuild configuration is centralized at the repository root, never duplicated per project: + +- **`Directory.Build.props`** carries the properties every project shares: the analyzer set and + `TreatWarningsAsErrors` from the zero-warnings policy above, plus `LangVersion`, + `TargetFramework` where uniform, and any repo-wide build metadata. A `.csproj` carries only what + is genuinely project-specific (`OutputType`, `IsPackable`, project references). +- **`Directory.Packages.props`** owns central package management: it sets + `ManagePackageVersionsCentrally` to `true` (in this file, not `Directory.Build.props`) and + declares every dependency version once as a `PackageVersion` item, so a `.csproj`'s + `PackageReference` items are versionless. One file to review on a bump, one Dependabot surface, + no version skew between projects. + +A repo whose projects still carry per-project analyzer settings or versioned `PackageReference` +items is drifted, move the shared property or version up to the root file rather than editing it +in place. + +### Build tasks + +Run these from VS Code's task runner (Terminal -> Run Task) or an agent's task-running tool. The +three clean-compile tasks are carried verbatim, and a repo adds its own convenience tasks (tool +updates, dependency upgrades, benchmarks) on top: + +- `.NET Build`: build with diagnostic verbosity *(clean-compile)* +- `CSharpier Format`: auto-format code with CSharpier *(clean-compile)* +- `.NET Format`: run CSharpier and build, then verify formatting and style with + `--verify-no-changes` *(clean-compile, the task to run after edits)* + +## Tooling and editor + +- **CSharpier** is the primary code formatter, invoked by the `CSharpier Format` task or + `dotnet csharpier format --log-level=debug .`. +- **`dotnet format`** verifies style: + `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. +- **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns + version management. +- CI is the authoritative lint backstop. A local pre-commit hook is strongly suggested: wire + Husky.Net from `catalog/snippets/husky/` for local enforcement, including the shared doc gates. +- **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings + without overrides. + +## Coding standards and conventions + +Key rules: no `var` (always explicit types), file-scoped namespaces, Allman braces, Nullable +enabled, modern C# features (primary constructors, pattern matching, collection expressions). Every +public surface has XML documentation. Private fields use `_camelCase`, static fields `s_camelCase`, +constants PascalCase. Member ordering follows StyleCop SA1201. + +For language features, naming, code structure, and XML documentation examples, see +`references/conventions.md`. + +## Analyzer suppressions (.NET) + +CODESTYLE.md's General section sets the suppression-scope order fleet-wide: narrowest scope first, +symbol-scoped before project-scoped before repo-wide, and only for a genuine false-positive or a +deliberate, documented exception, never a blanket relaxation to get a brownfield port to build. +The .NET mechanics, narrowest first: + +- **Never use `#pragma warning disable`** to silence an analyzer. +- **Symbol-scoped**: a `[System.Diagnostics.CodeAnalysis.SuppressMessage(...)]` attribute with a + `Justification`, on the specific member or type: + + ```csharp + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "https://github.com/dotnet/sdk/issues/51681" + )] + ``` + +- **Project-scoped** (e.g. a test project): a `dotnet_diagnostic.<RULE>.severity` entry in that + project's own `.editorconfig`, with a comment explaining why. +- **Repo-wide**: a `dotnet_diagnostic.<RULE>.severity` entry in the root `.editorconfig`, only + when the rule is genuinely not applicable to any project. Relaxing a batch of `CA*` rules (or + `dotnet_analyzer_diagnostic.severity`) to push a brownfield port through the build is exactly + what this forbids. + +## Error handling and logging + +1. **Structured logging**: use structured message templates. Serilog is the application's concrete + backend, and a library never references it directly (see item 2): + + ```csharp + logger.LogError(exception, "{Function}", function); + ``` + +2. **Libraries log through abstractions, never a concrete backend.** A NuGet library depends only + on `Microsoft.Extensions.Logging.Abstractions` and exposes an `ILoggerFactory` seam: a settable + global factory defaulting to `NullLoggerFactory.Instance` (fallback `NullLogger.Instance`) with + `SetFactory`/`TrySetFactory`, and/or an `ILoggerFactory`/`ILogger` parameter in its API. It + must not reference Serilog or any sink, which would force a logging framework on every consumer + and drag in AOT-incompatible dependencies. The consuming application owns the concrete logger + (Serilog is fine there), bridges it to `ILoggerFactory` (e.g. `SerilogLoggerFactory` from + `Serilog.Extensions.Logging`), and injects it. Reference pattern: a `LogOptions` seam in the + library, against which the consuming CLI builds the Serilog-backed factory and injects it via + `LogOptions.SetFactory`. +3. **CallerMemberName**: use for automatic function name tracking: + + ```csharp + public bool LogAndPropagate( + Exception exception, + [CallerMemberName] string function = "unknown" + ) + ``` + +4. **Logger extensions**: use `Extensions.cs` for logger and other extension methods: + + ```csharp + extension(ILogger logger) + { + public bool LogAndPropagate(Exception exception, ...) { } + } + ``` + +5. **Exceptions**: do not swallow exceptions, either log and rethrow or translate to a + domain-specific exception. + +## Code patterns + +1. **Guard clauses**: prefer early returns for validation and error handling. +2. **Async all the way**: avoid blocking calls (`.Result`, `.Wait()`), use `async`/`await`. +3. **Cancellation tokens**: accept `CancellationToken` as the last parameter and pass it through. +4. **ConfigureAwait**: in library code, use `ConfigureAwait(false)` unless context is required. Do + not call `ConfigureAwait(false)` in xUnit tests (see xUnit1030). +5. **Disposables**: use `await using` for async disposables, prefer `using` declarations. +6. **LINQ vs loops**: use LINQ for clarity, loops for hot paths or allocations. +7. **HTTP**: reuse `HttpClient` via factory, never per-request instantiation. +8. **Collections**: prefer `IReadOnlyList<T>`/`IReadOnlyCollection<T>` for public APIs. +9. **Immutability**: prefer immutable records, use init-only setters when records are not + suitable, and prefer immutable or frozen collections for read-only data. +10. **Exceptions as control flow**: avoid using exceptions for expected flow. +11. **Sealing classes**: seal classes that are not designed for inheritance. +12. **Lazy initialization**: use `Lazy<T>` for static, thread-safe instantiation (e.g. a logger + factory, an HTTP factory). + +## Testing conventions + +xUnit v3 (`xunit.v3`, not the legacy `xunit`) + AwesomeAssertions (`.Should()` API, never native +asserts). Arrange-Act-Assert pattern, descriptive underscore names, `[Theory]`/`[InlineData]` for +parameterized tests. See `references/testing.md` for the framework setup template. + +## Project configuration + +.NET 10.0 target, AOT-compatible (`IsAotCompatible=true`, `VerifyReferenceAotCompatibility=true`), +SourceLink, embedded untracked sources, `InternalsVisibleTo` for test/benchmark access. See +`references/project-config.md` for the full property list. + +## Best practices + +All changes go through pull requests. diff --git a/.github/skills/dotnet-codestyle/references/conventions.md b/.github/skills/dotnet-codestyle/references/conventions.md new file mode 100644 index 00000000..46897c81 --- /dev/null +++ b/.github/skills/dotnet-codestyle/references/conventions.md @@ -0,0 +1,136 @@ +# .NET Coding Standards and Conventions + +Code snippets below are illustrative examples only, replace namespaces and types to match your +project. + +## C# language features + +1. **File-scoped namespaces**: + + ```csharp + namespace Example.Project.Library; + ``` + +2. **Nullable reference types**: enabled (`<Nullable>enable</Nullable>`), use nullable annotations + appropriately, use `required` for mandatory properties. +3. **Modern C# features**: prefer modern language constructs, primary constructors when + appropriate, top-level statements for console apps, pattern matching over traditional checks, + collection expressions when types loosely match, extension methods (the classic + `this`-parameter form or an `extension(<receiver>) { ... }` block on C# 14+), implicit object + creation when the type is apparent, range and index operators. +4. **Expression-bodied members**: use for applicable methods, properties, accessors, operators, + lambdas, local functions. +5. **`var` keyword**: do NOT use `var`, always use explicit types: + + ```csharp + // Correct + int count = 42; + string name = "test"; + + // Incorrect + var count = 42; + var name = "test"; + ``` + +## Naming conventions + +1. **Private fields**: underscore prefix with camelCase: + + ```csharp + private readonly HttpClient _httpClient; + private int _counter; + ``` + +2. **Static fields**: `s_` prefix with camelCase: + + ```csharp + private static int s_instanceCount; + ``` + +3. **Constants**: PascalCase: + + ```csharp + private const int MaxRetries = 3; + ``` + +## Code structure + +1. **Global usings**: use `GlobalUsings.cs` for common namespaces: + + ```csharp + global using System; + global using System.Net.Http; + global using System.Threading.Tasks; + global using Microsoft.Extensions.Logging; + ``` + +2. **Usings placement**: outside the namespace, sorted with `System` directives first: + + ```csharp + using System.CommandLine; + using System.Runtime.CompilerServices; + using Example.Project.Library; + + namespace Example.Project.Console; + ``` + +3. **Braces**: Allman style: + + ```csharp + public void Method() + { + if (condition) + { + // code + } + } + ``` + +4. **Indentation**: C# files 4 spaces, XML/csproj files 2 spaces, YAML files 2 spaces, JSON files + 4 spaces. +5. **Line endings**: not specified here, governed per repo by `.editorconfig` / `.gitattributes` + per GOVERNANCE.md's "Line Endings" section. +6. **`#region`**: do not use regions, prefer logical file/folder/namespace organization. +7. **Member ordering (StyleCop SA1201)**: const -> static readonly -> static fields -> instance + readonly fields -> instance fields -> constructors -> public (events -> properties -> indexers + -> methods -> operators) -> non-public in same order -> nested types. + +## Comments and documentation + +XML documentation is on: `<GenerateDocumentationFile>true</GenerateDocumentationFile>`, and +missing XML comments for public APIs are suppressed in `.editorconfig`. Every public surface must +still be documented: a single-line summary, additional details in remarks, documented input +parameters, return values, exceptions, and crefs. + +```csharp +/// <summary> +/// Example of a single line summary. +/// </summary> +/// <remarks> +/// Additional important details about usage. +/// Multiple lines if needed. +/// </remarks> +/// <param name="category"> +/// The quote category to request +/// </param> +/// <param name="cancellationToken"> +/// A <see cref="System.Threading.CancellationToken"/> that can be used to cancel the request. +/// </param> +/// <returns> +/// A <see cref="string"/> containing the quote text. +/// </returns> +/// <exception cref="System.ArgumentException"> +/// Thrown when <paramref name="category"/> is not a supported value. +/// </exception> +public async Task<string> GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) +{ + if (category is not ("motivational" or "humor")) + { + throw new ArgumentException($"Unsupported category: {category}", nameof(category)); + } + + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(1, cancellationToken); + return $"Quote for {category}"; +} +``` diff --git a/.github/skills/dotnet-codestyle/references/project-config.md b/.github/skills/dotnet-codestyle/references/project-config.md new file mode 100644 index 00000000..42b8fd1c --- /dev/null +++ b/.github/skills/dotnet-codestyle/references/project-config.md @@ -0,0 +1,21 @@ +# .NET Project Configuration + +1. **Target framework**: .NET 10.0 (`<TargetFramework>net10.0</TargetFramework>`). +2. **AOT compatibility**: `<IsAotCompatible>true</IsAotCompatible>`, + `<VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility>`. +3. **Assembly information**: use semantic versioning, include SourceLink + (`<PublishRepositoryUrl>true</PublishRepositoryUrl>`), embed untracked sources + (`<EmbedUntrackedSources>true</EmbedUntrackedSources>`). +4. **Internal visibility**: use `InternalsVisibleTo` for test and benchmark access (adapt the + project names to your repo's test/benchmark projects): + + ```xml + <ItemGroup> + <InternalsVisibleTo Include="YourBenchmarkProject" /> + <InternalsVisibleTo Include="YourTestProject" /> + </ItemGroup> + ``` + +5. **Nullable and XML documentation**: `<Nullable>enable</Nullable>`, + `<GenerateDocumentationFile>true</GenerateDocumentationFile>` (see `references/conventions.md` + for the XML documentation format every public surface needs). diff --git a/.github/skills/dotnet-codestyle/references/testing.md b/.github/skills/dotnet-codestyle/references/testing.md new file mode 100644 index 00000000..5a84a178 --- /dev/null +++ b/.github/skills/dotnet-codestyle/references/testing.md @@ -0,0 +1,25 @@ +# .NET Testing Conventions + +1. **Framework**: xUnit v3 or later (the `xunit.v3` package, never the legacy v2 `xunit` package) + with AwesomeAssertions for every assertion. Native xUnit asserts (`Assert.Equal`, + `Assert.True`, ...) are not allowed, use the fluent `.Should()` API. Dynamic test skipping + (`Assert.Skip`, `Assert.SkipWhen`) is control flow, not an assertion, and stays native: + + ```csharp + [Fact] + public void MethodName_Scenario_ExpectedBehavior() + { + // Arrange + int expected = 42; + + // Act + int actual = GetValue(); + + // Assert + actual.Should().Be(expected); + } + ``` + +2. **Organization**: Arrange-Act-Assert pattern. +3. **Naming**: descriptive names with underscores. +4. **Theory tests**: use `[Theory]` with `[InlineData]`. diff --git a/.github/skills/drive-pr/SKILL.md b/.github/skills/drive-pr/SKILL.md new file mode 100644 index 00000000..1e3fb1f7 --- /dev/null +++ b/.github/skills/drive-pr/SKILL.md @@ -0,0 +1,131 @@ +--- +name: drive-pr +description: >- + Drives a ptr727/ProjectTemplate fleet pull request through its review loop, feature branch into + develop and, when asked, on to a mergeable develop -> main promotion PR, applying the + pr-review-conduct disposition to every reviewer finding along the way: fix it, decline it with + evidence, defer it behind a filed issue, or put the call to the maintainer and wait for an + explicit answer in the same turn. Use this whenever asked to drive, land, take, chase, or push + a PR toward develop or main, or to run the review loop hands off instead of narrating each + round. When the request does not say how far ("drive this PR", "land it"), ask once whether the + target is develop or a mergeable main promotion PR, rather than guessing. Triggers even when + only one PR is named, because a finding raised against the develop -> main promotion PR + routinely needs its own feature -> develop fix cycle before the promotion PR can go green, and + stopping at the first promotion-PR finding is the early exit this skill exists to prevent. Ends + at develop merged, or at a promotion PR meeting the pr-review-conduct Merge Gate, never merges + main itself, that is the separate merge-and-release skill, its own go-ahead. +--- + +# Drive PR + +## Why This Exists + +The same request repeats every time a change is ready: drive it through review, resolve whatever +a reviewer raises, and keep going until develop, or main, actually has it. Re-explaining the +finding-disposition policy and the promotion-PR wrinkle each time is the cost this skill removes. +The wrinkle: a finding raised against the develop -> main promotion PR usually cannot be fixed on +that PR directly, its diff is develop's diff against main, so the fix lands as its own +feature -> develop PR first. Stopping at the first such finding, or forgetting to loop back to the +promotion PR once the fix lands, is the early exit this skill exists to prevent. + +## How Far to Drive + +- Read the invocation for an explicit target first. "To develop" or "to dev" means stop once + merged into develop. "To main", "through to main", or "all the way" means continue to a + mergeable promotion PR. Act on either without asking. +- When the request names no target ("drive this PR", "land it", "take this PR"), ask once, + before the first push: develop only, or all the way to a mergeable main promotion PR. Recommend + "all the way to main" as the default, a promotion PR left to go stale once develop is ready is + the more common regret than driving one step too far. +- A repo on the operational workflow model (registry `workflowModel: operational`) has no + standing promotion PR expectation, confirm whether a promotion PR is even wanted before opening + one, per operational-vs-release-workflow's "Operational repositories" delta. + +## What Invoking This Skill Authorizes + +- Naming this skill, and answering its how-far question, is the maintainer's explicit, current + go-ahead for every feature -> develop squash merge the drive performs to reach that target. +- It is never authorization to merge the develop -> main promotion PR, or to dispatch a release. + Those stay in merge-and-release, invoked on its own so the maintainer keeps a checkpoint before + the harder-to-reverse step. +- The pr-review-conduct Merge Gate still gates every merge this skill performs on its own. The + go-ahead removes the "may I merge to develop" question, not the gate itself, a feature PR with + an open finding does not merge regardless of target. + +## The Drive Loop + +1. Isolate into a worktree per repo-worktree, based on develop, before the first edit. +2. Run `local-strict-review` against the branch's current diff, then push the branch and open + the feature -> develop PR if it does not exist yet. +3. Drive pr-review-conduct's review loop on it to the Merge Gate, disposing of every finding per + "Disposing of Every Finding" below. +4. Capture the branch's own tip before merging, `gh pr view [number] --json headRefOid --jq + .headRefOid`, needed for the verify-then-delete step below since `gh pr merge` itself reports + the resulting squash commit on `develop`, not the PR's `headRefOid`. Merge the feature PR into + develop, `gh pr merge [number] --squash --repo owner/repo`. Never `--delete-branch` on this + call, it is run from inside the task's own worktree per step 1, where the feature branch is + checked out, and `gh pr merge --delete-branch` needs to switch that worktree to the base branch + to delete it, which fails when `develop` is already checked out somewhere else, the ordinary + case in this layout. Instead run repo-worktree's post-merge cleanup from the base clone: remove + the worktree, delete the now-merged local task branch, then verify before deleting the remote + one, `git ls-remote --heads --exit-code -- origin "refs/heads/<branch>"` matches the + `headRefOid` captured above, `--` before `origin` and the fully-qualified ref. `--heads origin + "<branch>"` alone still tail-matches a differently-prefixed branch sharing the same suffix, and + `--` placed after `origin` instead of before it is not equivalent either, verified empirically + against a `refs/heads/other/--` ref: after-origin also matched it, before-origin matched only + the one intended. `--exit-code` distinguishes exit `2`, branch genuinely gone, from any other + non-zero exit, a failed query, an unreachable remote and a gone branch both print nothing to + stdout otherwise. Stop and report either a mismatch or a failed query rather than deleting, + someone could have pushed to the branch after the merge, or the name could have been reused. + `<branch>` is the real value, substituted as its own quoted argument (a shell variable + expansion such as `"$branch"`, or an argv element), never handed to `eval` or `sh -c` for a + second round of shell parsing, the only way an embedded `$()` or backtick would actually run. + A valid ref can start with `-` or carry a shell metacharacter, which is why it stays quoted + regardless. Only once it matches, `git push origin --delete -- "<branch>"`. Never + `--force-with-lease` here, git-commit-conventions forbids it + unconditionally, this plain verify-then-delete is the safety gate, not a compare-and-swap at + delete time. The + repo's auto-delete-head-branches setting is kept off fleet-wide (to protect `develop` and + `main` from it, GitHub has no per-branch exception), so nothing deletes an ordinary feature + branch automatically. Stop here and report the merged PR when the target is develop only. +5. Open the develop -> main promotion PR if it does not exist yet, or find the existing one. +6. Drive its review loop the same way. A finding that needs a code change never gets pushed to + the promotion PR directly, its head is develop, so land the fix as a fresh pass through steps + 1 to 4 in its own worktree and branch, then return here. +7. The fix landing on develop updates the promotion PR's diff and head SHA on its own, re-request + a review on the new head and continue the loop. +8. Repeat 6 and 7 until the promotion PR itself carries no open finding and its checks are green + on the current head. +9. Report the promotion PR number and its ready state. Do not merge it. + +## Disposing of Every Finding + +pr-review-conduct's five outcomes are the actual rule, this is the mapping to use while driving: + +- Real, so fix it. Run `local-strict-review` against the branch's current diff, push it, reply + with its commit SHA (outcome 1). +- Not real, or real but out of scope here, so decline in the thread with evidence: the command + and its output, the code path, or the rule that governs it. An assertion never closes a finding + on its own (outcome 2). +- Real and worth doing, but later, so file the issue first, then reply with its link (outcome 4). +- Real, fixable, but a value call rather than a scope boundary, or the agent genuinely does not + know which of the above applies, so ask the maintainer directly, whatever the runtime's own + interactive-question mechanism is, and get an explicit answer in the same turn, a plan to ask + later is resolution by silence (outcome 3). +- The same finding keeps recurring against correct code, fix the class, sharpen a name, add a + comment, or take the rule itself to the maintainer, rather than re-arguing the instance every + round (outcome 5). + +## Mechanics Live Elsewhere + +- Review loop mechanics, the Merge Gate, and `scripts/pr_review.py`: pr-review-conduct. +- Branch rules, never delete develop, the EOL-only conflict, issue-closing keywords belonging on + the promotion PR: operational-vs-release-workflow. +- Worktree isolation and post-merge cleanup: repo-worktree. + +## Stop and Ask, Beyond the How-Far Question + +- A genuine design trade-off, a recurring finding pattern, or an architectural redesign proposal + each escalate per pr-review-conduct's own list, restated there, not duplicated here. +- An unrecognized review shape blocks the gate on its own, file an issue naming it and ask, never + guess what new wording probably meant. diff --git a/.github/skills/fleet-conformance-check/SKILL.md b/.github/skills/fleet-conformance-check/SKILL.md new file mode 100644 index 00000000..2b6e8406 --- /dev/null +++ b/.github/skills/fleet-conformance-check/SKILL.md @@ -0,0 +1,74 @@ +--- +name: fleet-conformance-check +description: >- + Checks, from inside a downstream repo's own session, whether this repo and this machine are + current against the ptr727/ProjectTemplate hub, and safely self-applies what it can. Use this + whenever asked to check if this repo is up to date with the hub, whenever a fleet rule or Skill + seems to not be applying and the cause is unclear, or whenever about to work in a fleet repo and + wanting to confirm the ground under that work is current before trusting it. Needs no standing + hub checkout of its own and no named target repo, only the repo the session is already in, + though the check itself fetches a hub checkout to reach scripts/skills_install.py, since + scripts/ is hub-hosted rather than carried. This is the counterpart to resync-a-repo, which + needs both a hub checkout already in hand and a named external target to drive change from the + hub side instead. Also triggers on "why do I have to keep restating this rule every session," + since a stale or missing Skills install is the most common cause and the cheapest one to rule + out first. +--- + +# Fleet Conformance Check + +## Why this exists + +A downstream repo today only finds out it has drifted when someone runs a hub-driven resync +against it by name. Nothing notices from the inside on its own. This skill is that inside check, +run with no hub-side operator watching, so a stale Skills install or an out-of-date `AGENTS.md` +pointer gets noticed and fixed without waiting for a fleet-wide sweep to reach this particular +repo. + +## What it checks + +1. **Is the Skills install current on this machine.** `scripts/` is hub-hosted and reached rather + than carried, per GOVERNANCE.md "Hub-Hosted Tooling", so fetch a hub checkout + (`github.com/ptr727/ProjectTemplate`, `main` branch, fetched fresh) and run + `python3 scripts/skills_install.py --report` from it. A stale or missing stamp is very often + the direct answer to "why isn't a fleet rule applying": the harness never loaded the current + content in the first place, and no amount of re-reading `GOVERNANCE.md` fixes that. +2. **Does this repo's own carried content still match the hub.** Compare `AGENTS.md`'s + "Where the Rules Live" pointer text, and any other verbatim `AGENTS.md`/`GOVERNANCE.md` section + this repo carries, against the same hub checkout's current wording, by reading the text rather + than by feel. + +## What it is safe to fix on its own + +- **Re-run the installer**, `python3 scripts/skills_install.py`, when the stamp reports stale. + This is a per-machine, local-only change, nothing in it touches this repo's git history or + needs a review. + +Nothing else. This skill never re-vendors a carried file, never deletes one, and never applies a +setting or ruleset. Those are `resync-a-repo`'s job, driven from the hub with a named target, +never a downstream repo acting on itself. + +## Refresh cadence + +Re-run the installer when `--report` exits non-zero, and after any hub merge that touches +`.agents/skills/`. Session entry runs no automatic check, by design: the trigger is suspicion, +and the restated-rule symptom below is the loudest form of it. `docs/host-setup.md` +"Fleet Skills Install" in the hub states the same cadence for the host side, and an automated +refresh stays out of scope until the fleet has evidence the manual cadence fails. + +## What it escalates instead of touching + +- **A carried section that differs from the hub in a way that reads as a genuine local addition** + rather than plain staleness, the exact case `carried-instruction-file-guard` exists to protect. + Report precisely what differs and stop there. Per AUDIT.md, a downstream repo does not write its + own audit report or resync itself against the hub, it names what it found and points at + `resync-a-repo`, run from a hub checkout, as the next step. +- **Anything the installer alone cannot resolve**, a broken `claude` CLI marketplace + registration, a settings or ruleset drift, a workflow interface mismatch. Name it and hand it to + the maintainer or a hub-driven resync rather than patching around it locally. + +## Answering "why isn't a fleet rule applying" + +Check the install stamp first, before assuming a Skill's description is worded wrong or that the +rule was never carried to this repo at all. It is the most common cause, and it is the cheapest +one to confirm. diff --git a/.github/skills/git-commit-conventions/SKILL.md b/.github/skills/git-commit-conventions/SKILL.md new file mode 100644 index 00000000..5c1454f1 --- /dev/null +++ b/.github/skills/git-commit-conventions/SKILL.md @@ -0,0 +1,167 @@ +--- +name: git-commit-conventions +description: >- + Governs how an agent stages, commits, signs, and pushes in a ptr727/ProjectTemplate fleet repo: + default-to-staging vs. explicit commit authorization, why "commit" means commit-and-push, the + mandatory signed-commit and noreply-identity checks, never force-pushing, how a history rewrite + must re-identify a commit that is not the agent's own, and the destructive-git-command ban. Use + this whenever about to run git add/commit/push, whenever authorization to commit is ambiguous + ("fix this" versus "commit this"), whenever about to configure or verify commit signing or + git user.email, whenever a merge conflict or a stale branch tempts a force-push or a hard reset, + and whenever rewriting history (filter-repo, an interactive rebase equivalent) touches a commit + authored or committed by someone else. Triggers even when the task looks like routine + housekeeping, such as "clean up this branch" or "just push it", because a scope-widened commit + authorization, an unsigned commit, a fabricated identity, or a force-push are each easy to do by + habit and each one is a hard-to-reverse mistake on a shared branch. +--- + +# Git Commit Conventions + +## Why this exists + +These are the fleet's mechanical git rules for producing a commit, kept in one place instead of +re-derived per repo or per session: whether to commit at all, what committing implies, how +signing and identity are verified rather than configured, and which commands are never run +without being asked. None of these are style preferences. Branch protection enforces several of +them at push time, and the rest guard against damage a rejected push does not undo (a +scope-widened commit, a rewritten shared history, a destructive reset). + +## Staging versus committing + +- **Default to staging, not committing.** Stage with `git add` and leave `git commit` to the + developer unless the developer has explicitly authorized committing for the current ask ("commit + this", "open a PR"). Authorization is scope-bound: it covers the commits that specific task + needs, not a blanket license for the rest of the session. +- **Stage by explicit path, never `git add -A` or `git add .`.** A blanket add stages whatever + else happens to be in the tree, and what it sweeps in is another task's uncommitted work, + landing in a commit whose subject never mentions it, committed by a session that never saw it. + That sweep has happened, which is why task isolation exists (the `repo-worktree` skill), and + isolation makes a shared tree rare rather than impossible. Name the files this task changed, + and let anything else stay unstaged. +- **"Commit" means commit and push.** An authorization to commit carries the push to the feature + branch the work belongs on, because nothing reviews a local commit. The Copilot review loop, the + required status checks, and the maintainer all read the remote, so stopping at `git commit` + leaves the review unstarted and the branch's state private to one machine, which reads as + progress while none of the gates have run. Push to the feature branch, never to a protected + branch, and never with `--force`. Holding a commit locally is the narrower case: it happens when + the developer asks for it, not by default. +- **Check `git status` before committing, and treat any change this session did not make as a + stop.** The maintainer hand-edits files live, often `README.md`/`HISTORY.md`, sometimes with an + editor's LF -> CRLF flip on top, and a sibling agent session sharing the tree leaves its edits + the same way. Whoever the author is, a change this session did not make is never bundled: ask + whether to include it, or leave it unstaged and say so, rather than committing half-finished + work or stranding it in an unrelated commit. An unexpected change in the tree is also the + signal to re-check isolation per the `repo-worktree` skill, since it may mean another task is + live in this checkout. + +## Signing, verified not configured + +- **Every commit must be cryptographically signed (SSH or GPG).** Branch protection enforces this + on every fleet branch, and an unsigned commit is rejected on push. Signing depends on + environment configuration (`commit.gpgsign`, `user.signingkey`, `gpg.format`), but none of those + values prove signing actually works: `gpg.format=ssh` can sign straight from a key file with no + `ssh-agent` running at all (the common case on Git for Windows), just as GPG can sign + agent-backed or straight from a keyring. **Probing agent liveness (`ssh-add -L`, a `gpg-agent` + check) is not a valid test and must not be used.** It tests one specific delivery path, not + whether a commit actually ends up signed, and a host that signs straight from a key file fails + that probe while signing correctly. +- **Verify with a real scratch commit, read back with git's own verdict, not a text grep.** This + single probe is tech-agnostic (SSH agent-backed, SSH key-file, GPG agent-backed, and GPG keyring + all exercise the same code path) and doubles as the identity check below. Run it once before the + first agent-authored commit of a session. Don't assume a prior session left config correct. The + commit below is plain, deliberately no `-S`: forcing it would still succeed on a host where + `commit.gpgsign` is unset or false, which is the exact default-config gap this probe exists to + catch, since every real commit an agent makes is plain too: + + The probe is one physical line, not backslash-joined ones, so it copy-pastes cleanly into a + shell: + + ```sh + d=$(mktemp -d "${TMPDIR:-/tmp}/sign-check.XXXXXX") && ( trap 'rm -rf "$d"' 0; email=$(git config --global --get user.email) && git init -q "$d" && git -C "$d" commit --allow-empty -q -m check && out=$(git -C "$d" log -1 --format='sig=%G? author=%an <%ae> committer=%cn <%ce>') && echo "$out" && ae=$(git -C "$d" log -1 --format='%ae') && ce=$(git -C "$d" log -1 --format='%ce') && case "$out" in sig=G\ *|sig=U\ *) true ;; *) false ;; esac && case "$email" in *@users.noreply.github.com) true ;; *) false ;; esac && [ "$ae" = "$email" ] && [ "$ce" = "$email" ] ) + ``` + + PowerShell equivalent: + + ```powershell + $d = Join-Path $env:TEMP ([guid]::NewGuid()) + try { + $email = git config --global --get user.email + git init -q "$d" ` + && git -C "$d" commit --allow-empty -q -m check + $out = git -C "$d" log -1 --format='sig=%G? author=%an <%ae> committer=%cn <%ce>' + $out + $ae = git -C "$d" log -1 --format='%ae' + $ce = git -C "$d" log -1 --format='%ce' + if ($out -notmatch '^sig=[GU] ' -or $email -notmatch '@users\.noreply\.github\.com$' ` + -or $ae -ne $email -or $ce -ne $email) { + throw "signing/identity check failed: $out" + } + } finally { + if (Test-Path "$d") { Remove-Item -Recurse -Force "$d" } + } + ``` + + `sig` must read `G` (good signature) or `U` (good signature, unrecognized signer). For GPG, `U` + is a valid signature from a key whose trust level is merely undefined, common right after + generating a new key. For SSH, it's a valid signature from a key not found in the local + `allowed_signers` file, which doesn't affect whether GitHub itself verifies the commit, only + local `git verify-commit` output. `sig` is git's own verdict char. Don't grep localized + "Good" text, since that varies by git version and locale. Anything else, or the commit failing + outright, means **do not commit**: surface the actual error to the developer and stop at + `git add`. Nothing else is contrary evidence: not an unreachable agent, not a config value, not a + signature type you can't otherwise explain in past history (see below). +- **A mix of SSH- and GPG-signed commits in history is structural, not a host to track down.** + `git log --pretty='%G? %GK'` shows two distinct shapes, not two health states: a commit committed + by the PR's own author carries that host's own signature type, while a commit committed by + `GitHub <noreply@github.com>` is a squash-merge: GitHub creates and signs that commit itself, + server-side, with GitHub's own GPG key, regardless of what the PR author signed with locally. + Every commit on `develop`/`main` past its first squash-merge shows `GitHub` as committer and a + GPG signature. That's expected on every fleet repo, on every host, and is not evidence anything + is misconfigured. Check `commit.committer.name` before treating a differing signature type as a + clue worth chasing. +- **Signing must be live before the *first* commit, not retrofitted.** Turning on a + require-signed-commits rule against a branch that already carries unsigned commits forces a + rewrite of that entire history to re-sign it, changing every commit SHA and making whoever does + the rewrite the committer and signer of every commit in it (a rebase preserves `author` but not + the original signatures, and one contributor cannot sign for another). During new-repo setup, + never create commits until signing is verified. + +## Identity, verified not set + +**Commit under the committing account's own GitHub `noreply` identity, never a private, personal, +or invented address.** `author` and `committer` on every agent-authored commit are the GitHub +`noreply` address of the account whose key signs the commit, in `username@users.noreply.github.com` +or `ID+username@users.noreply.github.com` form. **Verify it, do not set it**: the scratch commit +from the signing check above already proves this end-to-end. Read its `author=`/`committer=` +output rather than trusting `git config --get user.email` alone, since a global config value +doesn't prove what actually lands on a commit object, and read both rather than the author alone +since a rebase, amend, or cherry-pick can rewrite the committer while leaving the author +untouched. Match both against that address before committing, rather than +writing a repo-local override. The identity is host configuration set globally once, so a repo-local +`user.email` is redundant where the global is right and a silently-shadowing wrong identity where +it is not. A mismatch is a host fault to surface to the maintainer, not to patch per repo, because +a local override hides a broken host that then commits wrong in every other repo on that machine. +A wrong identity is not cosmetic: a private email trips GitHub's email-privacy push protection, and +an invented author pollutes history. It is also a distinct failure from signing (a wrong author +does not by itself fail the signature check), though the ad-hoc identities that produce one are +typically also unsigned, which the signing rule above then rejects independently. + +## Never force push + +Do not run `git push --force` or `git push --force-with-lease` under any circumstances. Force +pushing rewrites shared history and can cause data loss. This holds regardless of how confident +the rewrite looks, a rejected push is recoverable, a force-pushed one is not. + +## History rewrites re-identify only what changed + +**Do not rewrite a commit that does not need to change.** A history rewrite (e.g. `git filter-repo` +to strip PII) re-signs every touched commit with the rewriter's key. If that commit is still +committed by a bot (`dependabot[bot]`, `github-actions[bot]`) or GitHub's own web-flow, the +signature will not match the committer and the require-signed-commits rule rejects it. Scope the +rewrite to only the commits that must change. Set `committer` (and `author`) to the rewriter's +identity on any non-own commit that must be modified. Verify with `git log --show-signature` after +any rewrite. See `references/history-rewrite.md` for the full two-gate rule. + +## Never run destructive git commands without being asked + +`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`, and other commands that discard work require explicit developer instruction. Never use them as a convenience inside a larger task. One narrow cleanup exception applies to `git branch -D <exact-task-branch>` after a squash merge. It requires live proof that the pull request for that exact branch merged and a clean worktree at the verified head SHA, per `repo-worktree`. The exception never applies to `develop`, an unmerged branch, an unresolved pull request, or a branch with uncommitted work. diff --git a/.github/skills/git-commit-conventions/references/history-rewrite.md b/.github/skills/git-commit-conventions/references/history-rewrite.md new file mode 100644 index 00000000..b331a557 --- /dev/null +++ b/.github/skills/git-commit-conventions/references/history-rewrite.md @@ -0,0 +1,24 @@ +# History Rewrites: Re-identification Rules + +**A history rewrite includes only the commits that must change, and re-identifies any commit it +rewrites that is not the agent's own.** Filtering history (`git filter-repo` or an equivalent, for +example to strip PII) re-signs every commit it touches with the rewriter's own key, while the +tooling preserves each commit's original `author`/`committer` unless told otherwise. GitHub +verifies a signature against the commit's `committer` identity, so a signature from the rewriter's +key over a commit still committed by a bot (`dependabot[bot]`, `github-actions[bot]`) or GitHub's +own web-flow does not match its committer and lands `unknown_key`/unverified, which a +require-signed-commits rule then rejects. + +Two gates keep committer and signature aligned: + +1. **Scope the rewrite to only the commits that must be modified.** By default those are the + rewriter's own, whose committer already matches, so a commit that needs no change stays out of + the rewrite entirely and its identity and signature are never touched. +2. **If a commit that must change is not the rewriter's own, set its `committer` to the rewriter's + own signing identity before re-signing** (and its `author` too, since a rewrite that alters + content should not keep attributing it to the bot). The original bot attribution is deliberately + given up as the cost of having to rewrite it. + +Never leave a signature over a commit committed by another identity. Verify after any rewrite that +every rewritten commit is signed and committed under the correct identity +(`git log --show-signature`). diff --git a/.github/skills/local-strict-review/SKILL.md b/.github/skills/local-strict-review/SKILL.md new file mode 100644 index 00000000..cad9a06e --- /dev/null +++ b/.github/skills/local-strict-review/SKILL.md @@ -0,0 +1,71 @@ +--- +name: local-strict-review +description: >- + Runs one read-only, adversarial review pass against this branch's current diff against its + target branch, full file context included, on the strongest model tier the session can reach, + before a unit of work is pushed toward a pull request or claimed done. Use this whenever staged, + committed, or untracked work is about to be pushed on a PR-bound branch, and whenever + `agent-conduct`'s "about to claim work is done, verified, green, or fixed" trigger fires for + PR-bound work. Triggers even when the change looks small or the same session already judged its + own diff ready, because a self-review pass judging its own diff inherits its own blind spots, + the exact gap this skill exists to close before a PR-hosted reviewer closes it instead. Reuses + `code-review`'s "Review the Change" criteria rather than restating them, and owns only this + local, pre-PR moment. Once a pull request exists, `pr-review-conduct` and `drive-pr` own + triaging and disposing of what a PR-hosted reviewer finds. +--- + +# Local Strict Review + +## Why This Exists + +A coding agent that finishes a unit of work, judges it ready, and opens the pull request is judging its own diff with the model, and often the blind spots, that wrote it. CodeRabbit, Qodo, and Copilot routinely find real defects that a local pass missed, and each round costs review latency and, for a rate-limited reviewer, shared account-wide quota. A local, full-file-context adversarial pass before the pull request exists catches the same class of defect for a fixed, smaller cost, the same reasoning that already runs local lint before a push instead of waiting for CI. + +## What It Does + +Dispatches one read-only subagent against this branch's full diff since it forked from its target branch. Resolve `<target>` once, `develop` unless `repo-worktree`'s base-branch rule put this branch on `main` instead, then fetch it, `git fetch origin <target>`, and diff against the merge-base, `git diff "$(git merge-base origin/<target> HEAD)"`. Stop and report a failed fetch rather than running the merge-base or diff commands anyway: an existing local `origin/<target>` ref can still resolve after a failed fetch, and reviewing against it silently trades the current target for a stale one. Use the same resolved `<target>` in every command below, never a literal `develop` alongside it. Naming the target branch explicitly matters: the branch's own `@{u}` tracking ref points at the branch's own remote once it has been pushed, not at the branch it targets, so anchoring there silently narrows a later run to only the diff since the last push instead of the full accumulated diff. That merge-base diff covers every commit already on the branch plus whatever is currently staged or unstaged, so it is never empty and never reviews only the latest increment, at any of the moments this skill is invoked from. A fresh review of the full accumulated diff is what catches what per-push review misses, the exact evidence this skill exists to act on. + +`git diff` never reports a path `git add` has not touched, so a newly created file sitting untracked would otherwise go unread. List it explicitly, `git ls-files --others --exclude-standard`, and read each result in full alongside the diff, the same as any other file the diff touches. + +The subagent reads the full content of every file the diff and the untracked-file list touch, not just the hunks, since cross-file and whole-file context is exactly what incremental review misses. It reports findings only. It never fixes, stages, or commits anything. + +Review criteria are `code-review`'s "Review the Change" section, reused rather than restated here, plus three traps worth calling out explicitly for a pass that runs before a human or a PR-hosted reviewer ever sees the diff: unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior differences. `code-review`'s separate "Publish Every Finding" section does not apply here: this skill has no PR to post a comment on and no coverage marker to close a review with, so its own report contract below replaces that section rather than extending it. + +## Running It + +Follow `AGENTS.md` "Context and Delegation Discipline"'s subagent briefing shape: + +```text +Task: adversarial review of this branch's diff against its merge-base with its target branch, + read full surrounding files where the diff hunks alone do not give enough context. +Paths: the files `git diff --name-only "$(git merge-base origin/<target> HEAD)"` and + `git ls-files --others --exclude-standard` list, mandatory floor. Reading a specific + unchanged caller or consumer beyond that list is in bounds only where a candidate finding's + proof actually depends on it, per code-review's own "follow data and control flow beyond the + edited lines" instruction below, never as an open-ended exploration. +Rules that bind this task: quote `code-review`'s "Review the Change" section into the prompt, + plus flag unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior + differences explicitly. Do not quote "Publish Every Finding", this task's report contract is + the Return line below, not a PR comment or a coverage marker. +Return: one finding per line, file:line, the concrete failure scenario, no severity theater. +Bounds: read-only. No edit, no stage, no commit, no push, no PR-hosted write of any kind. +<AGENTS.md's own unresolved-rule closing line, quoted verbatim from "Context and Delegation Discipline", not restated here> +``` + +**Model tier:** the strongest tier this session can reach, per `AGENTS.md` "Match the model tier to the judgment" and "Never tier down the seat holding the judgment", applied here to the reviewer rather than the author. Run the pass on the same tier that authored the change when only one tier is reachable, a second, adversarially-prompted look still catches what the authoring pass's own "looks ready" judgment did not. + +## Disposing of Findings + +Every finding maps to one of `pr-review-conduct`'s five outcomes before the pull request opens: fixed, evidence-disproven, filed as a deferred issue, escalated to the maintainer for an explicit call, or, if it keeps recurring, taken as a signal to fix the class. A finding this pass raised and not fixed is never the agent's own call to just leave. Per outcome 3, that decision needs the maintainer's explicit answer, the same way a PR-hosted finding would. Running this pass is expected before every push toward a pull request, per `agent-conduct`. Its findings stay advisory: a finding it raises does not by itself block `git commit` or `gh pr create`, the disposition above is what closes it, the same posture local lint holds today. It posts nothing to GitHub, it only reports to the session driving the work. A finding raised here and not fixed is not thereby resolved: the same finding shape reaching a PR-hosted reviewer later still gets its own fresh disposition, per `pr-review-conduct`'s "a disposition decided on one PR does not carry to the next." + +## When to Run It + +- Before the first push toward a pull request (`drive-pr`'s Drive Loop step 2, `pr-review-conduct`'s Expected review loop step 1). +- Before pushing a fix for a reviewer finding, the same self-review blind spot applies to a fix as to the original diff (`drive-pr`'s "Disposing of Every Finding", `pr-review-conduct`'s outcome 1). +- Whenever `agent-conduct`'s "about to claim work is done, verified, green, or fixed" trigger fires for work that will become, or already is, a pull request. + +## Mechanics Live Elsewhere + +- Review criteria: `code-review`. +- Delegation shape and model-tier discipline: `AGENTS.md` "Context and Delegation Discipline". +- Branch base rule (`develop` unless the task is explicitly `main`-only): `repo-worktree`. +- Finding disposition once a pull request exists, the Merge Gate, `scripts/pr_review.py`: `pr-review-conduct`, `drive-pr`. diff --git a/.github/skills/merge-and-release/SKILL.md b/.github/skills/merge-and-release/SKILL.md new file mode 100644 index 00000000..3eb31c30 --- /dev/null +++ b/.github/skills/merge-and-release/SKILL.md @@ -0,0 +1,225 @@ +--- +name: merge-and-release +description: >- + Merges a ready develop -> main promotion PR for any ptr727/ProjectTemplate fleet repo and, when + asked, dispatches the release, in this hub always refreshing this machine's installed Skills + from the newly promoted content as part of that release step, never as a separate ask. Use this + whenever asked to merge main, ship a release, cut a release, or finish a promotion once its PR + is already green and fully resolved (produced by drive-pr or by hand). When the request does + not say how far ("merge main", "ship it"), ask once whether to merge only or merge and release, + rather than guessing which the maintainer wants this time. Triggers even when the phrasing is + as short as "merge main and release", because that already states the scope and is itself the + explicit, current go-ahead this skill acts on without asking again, though it never substitutes + for the pr-review-conduct Merge Gate, a promotion PR that is not actually green and fully + resolved gets reported and stopped on, not merged. +--- + +# Merge and Release + +## Why This Exists + +Once drive-pr (or a maintainer by hand) leaves a promotion PR ready, the same two steps follow +every time: merge it, and usually dispatch the release it unblocks. In this hub a promotion can +also change `.agents/skills` content this very session depends on, so the release step always +carries a Skills refresh with it there, never a separate branch to ask about, an ambiguous "merge +and release" on the hub must not leave the maintainer unsure whether Skills got refreshed. One +skill covers all of it, scoped down by what the maintainer actually asks for. + +## How Far to Go + +- Read the invocation for an explicit scope first. "Just merge" or "merge only" means stop after + the merge. "Merge and release", "ship it", or "cut a release" means also dispatch, and in this + hub also refresh Skills as part of that same step. Act on either without asking. +- When the request names no scope ("merge main"), ask once, before merging: merge only, or merge + and release. Recommend "merge and release" as the default on a release-model repo, a promotion + merged without its release is the more common regret there. Recommend "merge only" as the + default on an operational repo (registry `workflowModel: operational`), where a release is a + separate, deliberate dispatch rather than an automatic follow-on to a promotion, per + operational-vs-release-workflow's "Operational repositories" delta. +- Detect the hub automatically, `git remote get-url origin` or `gh repo view --json + nameWithOwner` naming `ptr727/ProjectTemplate`. There the release scope silently includes the + Skills refresh, a downstream repo never sees it, it has no `.agents/skills` of its own to + refresh. + +## What Invoking This Skill Authorizes + +- Naming this skill, and answering its how-far question, is the maintainer's explicit, current + go-ahead to merge the promotion PR and to perform the scope chosen, for the one repo and PR in + front of the agent. It is never a standing mode carried to the next PR. +- It is never permission to merge a PR that fails the Merge Gate. Re-verify the gate at + invocation time, a check from earlier in the session can be stale. + +## The Procedure + +1. Identify the open develop -> main promotion PR for this repo, stop and report if none is open. +2. From a hub checkout, `scripts/` is not carried into downstream repos, run `scripts/pr_review.py + status [number] --repo owner/repo` on it and confirm the pr-review-conduct Merge Gate. Stop + and report exactly what is missing rather than merging on a partial gate. +3. `gh pr merge [number] --merge --repo owner/repo`. Never `--delete-branch`, the promotion PR's + head is `develop`. +4. Confirm the merge landed, `mergedAt` set, `main`'s tip matching the merge commit. +5. When the chosen scope includes a release, first bring the hub checkout used for this procedure + current, `git fetch origin main`, and read this repo's `releaseTrigger` from that fetched tip + rather than a possibly-stale working tree copy, relevant when the target repo is the hub itself + and this exact promotion changed its own registry entry. Select the one matching entry + explicitly, falling back to the registry's own default when that entry sets no + `releaseTrigger` of its own, and stop and report rather than guessing when selection is not + exactly one match, on a non-1 count exit non-zero rather than returning empty with success, an + ambiguous or missing match must fail loud, not read as an empty value still safe to act on: `git + show origin/main:registry/repos.json | jq -r --arg name '<repo-name>' '(.repos | map(select(.name + == $name))) as $m | if ($m | length) == 1 then ($m[0].releaseTrigger // .defaults.releaseTrigger) + else error("expected exactly one registry entry for \($name), got \($m | length)") end'`. Two + cases, `none` versus anything else. When it + reads `none`, report that no + release is configured, dispatch and run-correlation (step 6) do not apply. Otherwise (`two-phase`, + `dispatch-only`, or `publish-on-merge` alike), dispatch explicitly, `gh workflow run + publish-release.yml --ref main --repo owner/repo`, or `--ref develop` only when the maintainer + explicitly asked for a prerelease dispatch instead. `publish-on-merge`'s automatic publish is + gated on the actor being the codegen App merging a Dependabot or codegen PR + (operational-vs-release-workflow's publishing rules), so an ordinary human promotion merge, + exactly what step 3 just did, never triggers it, this step's explicit dispatch is what actually + ships the release here, not a side effect of the merge. +6. Correlate the specific run this dispatch produced rather than assuming the newest one is it. + `gh run list --repo owner/repo --workflow publish-release.yml --branch main --event + workflow_dispatch --json databaseId,createdAt,headSha` (or `--branch develop` for a prerelease + dispatch), matched by `headSha` against the dispatched ref's tip (`main`'s tip confirmed in + step 4, or `develop`'s current tip for a prerelease) and by `createdAt` against the dispatch + time. `gh run list` can momentarily omit a just-created run, so a single query reporting zero + candidates is not yet "never started". Poll the list itself, within a bounded interval, until + exactly one candidate matches. A concurrent run of a different event on the same branch must + never be mistaken for this one, more than one candidate is as inconclusive as zero. A run whose + `headSha` does not match the expected tip at all, rather than simply being absent, means the + dispatched ref moved between step 4's confirmation and the dispatch itself, report that + distinctly, the ref changed mid-dispatch, rather than folding it into an ordinary absent-run + timeout. Report and stop rather than guessing once the interval elapses with zero or more than + one candidate still matching. Only once exactly one candidate is confirmed, poll that one run + id to completion in one further bounded background wait with an explicit, finite timeout, + 2700 seconds (45 minutes, matching `scripts/pr_review.py`'s own default) unless the maintainer + states a different bound for this specific release: `timeout 2700 gh run watch <run-id> --repo + owner/repo --exit-status` on a host with GNU `timeout`, or the equivalent bounded-wait + mechanism enforcing the same bound on a host without it (macOS without coreutils, native + Windows). Report a timeout separately from a completed run's own conclusion, the tag or + version it produced. A run that fails, times out, or never starts is reported, never silently + retried. +7. In the hub, when the chosen scope includes a release, bring this checkout to the merged + content without discarding or mixing in anything local. First assert `git status --porcelain + --untracked-files=all --ignored -- .agents/skills/ .claude-plugin/` is empty, and stop and + report rather than proceeding over any uncommitted content there, tracked, untracked, or + gitignored, since `skills_install.py` reads both paths: `shutil.copytree()` installs each + `.agents/skills/` skill directory for Codex/opencode, and `claude plugin marketplace add` + installs from `.claude-plugin/` for Claude Code, so a gitignored stray file under either rides + along the same as any other, and the plain porcelain form (silent on ignored paths) would pass + this preflight while one still rides into an install. Scoped to those two paths rather than + the whole tree, matching `skills_install.py`'s own `source_ref()` dirty check (`watched = + [SKILLS_SRC, CLAUDE_PLUGIN_DIR]`), since an ignored file elsewhere in the checkout (a build + cache, a lockfile) is not this preflight's concern and should not block the refresh on it. + Then `git fetch origin main`, `git checkout main` + (or `git checkout -b main origin/main` the first time this checkout carries no local `main` at + all, `checkout` rather than `switch` since the fleet's own `git` floor is undeclared and + `checkout` needs no minimum version for this), and `git merge --ff-only origin/main`. + `checkout` still refuses a `main` checked out in another worktree, and `--ff-only` refuses + anything but a clean fast-forward, so either stops and reports on top of what the preflight + already ruled out, per Repository Boundaries and Write Safety. `--ff-only` does not fail when + local `main` is already ahead of `origin/main`, since a strict superset needs no fast-forward + and reports up to date, so assert `git rev-parse main` equals `git rev-parse origin/main` + afterward and stop and report on a mismatch, a local-only commit this checkout never pushed is + exactly the case a bare "up to date" would hide. `skills_install.py` stamps and installs from + whatever this checkout's HEAD already is, so running it against a stale, unrefreshed, or + locally-diverged `main` skips the refresh silently. Only then run `python3 scripts/skills_install.py --report`, then + `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, + regardless of whether step 5 or 6 dispatched, skipped, or failed a release, this step is gated + only on the chosen scope, never on the release outcome. This refreshes only the machine running + this session, per skill-lifecycle, every other machine still refreshes on its own next run or + `docs/host-setup.md` "Fleet Skills Install" cadence. +8. Run cleanup regardless of how steps 5 through 7 ended, no release configured, a dispatch + failure, an ambiguous run match, a timeout, a failed run, or a hub Skills refresh all still + reach this step, the merge in step 3 already landed by then. Two parts, both required, neither + optional: + - The promotion PR's own worktree: fetch and prune, remove the worktree, then fast-forward the + base clone to `develop`. Removing first, not after, matters: the base clone cannot check out + `develop` while the promotion worktree still has it checked out, one branch checked out in + two worktrees at once is refused outright. Never delete `develop`, it is the promotion PR's + own head, and the repo's auto-delete-head-branches setting is kept off fleet-wide for exactly + this reason, so nothing does this automatically. + - A defensive sweep for anything drive-pr's own cleanup should already have removed but might + not have, an interrupted loop, a fix landed by hand outside that skill, or a maintainer + merge in the GitHub UI. `git worktree list` for any worktree still registered under this + task's feature branches, `git branch -vv` for any local feature branch, `git ls-remote + --heads origin` for any matching remote feature branch. For each, verify it finished by + reading GitHub's own state with the exact fields this check needs, not a bare listing, and + stop and report rather than guessing when selection is not exactly one match, on a non-1 + count exit non-zero rather than returning empty with success, an ambiguous or missing match + must fail loud, not read as an empty value still safe to act on: `gh pr list --head + "<branch>" --state merged --repo owner/repo --json + number,baseRefName,mergedAt,headRefOid,headRefName,headRepository --jq 'if length == 1 then + .[0] else error("expected exactly one merged PR for this head, got \(length)") end'`. + `--head` is expected to match exactly (verified against `gh` 2.97.0 on this repo, a bare + prefix of a real branch name returned nothing), but confirming `headRefName` equals `<branch>` + costs one field and is cheap insurance against a future `gh` behavior change, not a workaround + for a known partial-match case. Confirm `headRepository` is non-null and its `nameWithOwner` equals + `owner/repo`, the owner alone is not enough, a same-owner PR against an identically named + branch in a different repository must never pass this check either. Confirm `baseRefName` + is `develop` (a different merged pull request can share the same head branch name against a + different base, and that is never this sweep's target) and `mergedAt` is set. Compare tips + only where a remote branch actually exists. `git ls-remote --heads --exit-code -- origin + "refs/heads/<branch>"` is the exact-match form and must be, in that argument order. `--heads + origin "<branch>"` alone still tail-matches, a bare `topic/x` pattern also returns an unrelated + `other/topic/x` if one exists. `--` placed after `origin` instead of before it is not + equivalent either, verified empirically: with a `refs/heads/other/--` ref present, `--heads + origin -- "refs/heads/<branch>"` matched both that ref and the intended one, while `--heads -- + origin "refs/heads/<branch>"` matched only the one intended. Exit status is a tri-state, not a + stdin-emptiness check: `--exit-code` makes exit `2` mean query succeeded, branch gone, most + likely a prior cleanup attempt got interrupted after the remote delete but before the local + one, so skip straight to the local-tip check below and never attempt the remote delete a + second time. Exit `0` means it matched. Anything else is a failed query, a network or auth + problem, and stops and reports rather than being read as absence, an unreachable remote and a + genuinely gone branch both print nothing to stdout, only the exit code tells them apart. + Where the remote branch does exist, its tip must match that exact pull request's `headRefOid` + before its own delete proceeds, proving nothing landed on it since. Where a local branch + still exists too, its tip (`git rev-parse --verify "refs/heads/<branch>"`) must independently + match `headRefOid` before its own delete proceeds. Neither side needs the other to exist, a + prior interrupted attempt may have deleted one side already and left only the other, so + verify and delete whichever side is still there and skip whichever already is not, never + block one side's cleanup on the other side's absence. No `--` on `rev-parse`, verified + empirically: `git rev-parse -- "<branch>"` treats the argument after `--` as a path rather + than a revision and never resolves a SHA at all. The fully-qualified form needs no `--` + regardless, since `refs/heads/<branch>` never itself starts with `-`, and `--verify` fails + loudly rather than guessing when it does not resolve. Every branch or + worktree-path placeholder below is the real value, substituted as its own quoted argument + (a shell variable expansion such as `"$branch"`, or an argv element), never handed to `eval` + or `sh -c` for a second round of shell parsing, the only way an embedded `$()` or backtick + would actually run. A valid ref can start with `-` or carry a shell metacharacter, which is + why it stays quoted regardless. `--` marks the + end of options wherever a command supports it. + `git merge-base --is-ancestor <branch> develop` must never be used for either tip check, a + squash merge (drive-pr's own merge method) never makes the feature tip a literal ancestor of + `develop`, so the check reports every already-finished branch as unmerged. Only once GitHub + confirms it, and only when a local worktree or branch is still there to remove, remove the + worktree by its exact path (a dirty worktree stops cleanup rather than discarding uncommitted + work), `git worktree remove "<worktree-path>"`, `git worktree list` names it, then delete the + local branch. `git branch + -d` has the identical squash blindness as `git merge-base --is-ancestor` and refuses too, so + use `git branch -D -- "<exact-branch>"` here, safe only because the GitHub-state check just + proved that exact branch finished, the narrow post-squash exception git-commit-conventions + describes, never applied to an unverified branch. Then, only when the remote branch still + exists, delete it the same way, `git push origin --delete -- "<branch>"`. + Never `--force-with-lease` here, git-commit-conventions + forbids it unconditionally, the GitHub-state check just completed is the verification gate, + not a compare-and-swap at delete time. Never apply this sweep to `develop` or `main` + themselves, only to feature branches a drive-pr loop created. + +## Mechanics Live Elsewhere + +- The Merge Gate itself: pr-review-conduct. +- Never delete develop, no-op republish, the operational repos' dispatch-only model: + operational-vs-release-workflow. +- What the dispatch actually builds and publishes: workflow-ci-contract. +- Skills install and report semantics: skill-lifecycle. +- Cleanup mechanics: repo-worktree. + +## Stop and Report, Never Guess + +- A merge conflict, a newly failing check, or a gate item that regressed since drive-pr finished + are each a stop, report the exact state, never force or retry blindly. +- `gh pr merge` or `gh workflow run` failing is reported with its actual output, never + suppressed, never assumed harmless on the agent's side alone. diff --git a/.github/skills/operational-vs-release-workflow/SKILL.md b/.github/skills/operational-vs-release-workflow/SKILL.md new file mode 100644 index 00000000..9a9e9f25 --- /dev/null +++ b/.github/skills/operational-vs-release-workflow/SKILL.md @@ -0,0 +1,156 @@ +--- +name: operational-vs-release-workflow +description: >- + Governs how a ptr727/ProjectTemplate fleet repo branches, promotes, and publishes: the + feature -> develop -> main flow, squash-only vs. merge-commit-only branch protection, the two + develop -> main promotion traps (never delete develop, EOL-only conflicts), the two-phase + publish model (PRs smoke-test only, a human merge never auto-publishes), NBGV semantic + versioning, and the operational-repo delta (direct-to-develop commits, advisory CI, dispatch-only + release) that applies instead whenever the registry's workflowModel field for this repo reads + operational rather than release. Use this whenever choosing a target branch for a change, + promoting develop to main, resolving a develop -> main merge conflict, deciding whether a + release repo's config change needs a PR versus an operational repo's config change can commit + straight to develop, bumping version.json, adding or dropping a release target, or reasoning + about why a merge did or didn't trigger a publish. Triggers even when the request sounds like + ordinary git housekeeping ("just push this config fix", "merge develop into main", "cut a + release"), because the two workflow models genuinely differ (a direct-to-develop commit that is + correct in an operational repo is a rule violation in a release repo, and vice versa) and + applying the wrong one is not obviously wrong to a reader who only knows one of the two. +--- + +# Operational vs. Release Workflow + +## Why this exists + +Two workflow models exist because the underlying repos are two different things. Most fleet repos +ship versioned units of delivery, so they earn a feature -> `develop` -> `main` flow with real +release gates. A handful of repos instead track a live service's running state (Home Assistant, +ESPHome, Vantage, home automation configs) where the "release" is the config already committed, +not something built and shipped later. Applying the release model's ceremony to an operational +repo, or skipping the release model's gates on a repo that actually ships versioned artifacts, is +each wrong in its own repo and correct in the other, which is why this is one skill keyed on which +repo you're in rather than two skills that never talk to each other. + +## Which model this repo uses + +Read the registry `workflowModel` field for this repo (`release`, the default, or `operational`). +The rest of this skill's "Branching" and "Publishing" sections describe the `release` model. The +"Operational repositories" section below is the complete delta for `operational` repos. Anything +not mentioned there is unchanged. When in doubt which one applies, check `registry/repos.json` +rather than guessing from the repo's contents. + +## Branching (release model) + +- **GitHub's repository setting for "default branch" reads `main`, but `develop` is where work starts and where in-flight content lives.** A worktree or clone that defaults to "the default branch" lands on `main` and can silently miss content that has merged to `develop` but not yet been promoted. Before branching off a change, or asserting something absent from this repo, check `develop`, not just whichever branch a tool defaulted to. See GOVERNANCE.md "Verification Discipline" on naming the branch a "does not exist" claim was checked against, and the `repo-worktree` skill, which owns the worktree-creation moment this base-branch choice is made at. +- `develop` is the integration branch. Feature branches -> `develop` is **squash-only**, which + keeps `develop` linear. +- `develop -> main` is **merge-commit only** (no squash, no rebase). Merge commits preserve + `develop`'s commit list as a real second-parent reference on `main`, which lets the release + model attribute releases to the develop commits that produced them. Branch protection enforces + this: the `develop` ruleset allows only `squash`, the `main` ruleset allows only `merge`. +- All commits on both branches must be cryptographically signed (SSH or GPG), see + `git-commit-conventions`. Squash and merge commits created via the GitHub UI are signed by + GitHub's web-flow key. +- **`develop` is forward-only, with no `main -> develop` back-merges.** The `develop` ruleset's + squash-only setting physically blocks merge commits on `develop`. Any historical back-merge + commits in `git log` predate this rule and must not be repeated. +- **Never delete `develop`, and take the EOL-only conflict by taking develop's side.** A + promotion PR's head *is* `develop`, so `--delete-branch` deletes it. An EOL-only conflict on a + workflow YAML file resolves on a throwaway branch off `main`, not on `develop`. Full recovery and + conflict-resolution commands: `references/branch-protection-and-promotion.md`. +- **A merge or release ends with worktree cleanup and the base clone on current `develop`.** Run the `repo-worktree` post-merge procedure after a feature squash merge. Run it again after a promotion or release completes, unless the user explicitly asks to retain a checkout or branch. Remove finished task, conflict-resolution, installer, and release helpers. Never delete `develop`, and never leave the base clone on `main` merely because `main` was promoted or released. +- **Issue-closing keywords (`Closes #N`, `Fixes #N`) go in the `develop -> main` promotion PR, not + the feature -> `develop` PR.** GitHub auto-closes an issue only when the closing keyword merges + into the **default branch** (`main`), so a feature -> `develop` PR merge never fires it. + Reference the issue in the `develop` PR body if useful, but the actual closing keyword belongs on + the promotion PR. Closing by hand is the ordinary route wherever the keyword cannot fire (a + promotion that already merged without it, or completed work with no promotion imminent), not a + repair for a botched promotion, cite the squash SHA and re-read that commit before closing. +- **Neither ruleset requires branches to be up to date before merging**, for different reasons on + each branch (a graph-based check that would fail every release on `main`, a check that stalls + bot auto-merge on `develop`). Detail: `references/branch-protection-and-promotion.md`. +- **Configuring branch protection: import the committed ruleset payloads, don't hand-build them.** + Exactly two rulesets, named `develop` and `main`. Full procedure, including the operational + `develop` payload and the brownfield-repo signing caveat: + `references/branch-protection-and-promotion.md`. +- **Dependabot and codegen target both `main` and `develop` in parallel**, each branch absorbing + its own bot PRs independently so neither falls behind, with the merge-bot dispatching the merge + form (`--squash`/`--merge`) that matches each PR's base ruleset. Codegen output must be + deterministic from its inputs alone, never per-run state, or the two branches' legs conflict on + every promotion. Full mechanics: `references/branch-protection-and-promotion.md`. +- **App-token workflows authenticate with Client ID, not the deprecated App ID.** Use + `client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}` at any new App-token call site. + +## Publishing (release model) + +- **The two-phase model is the default: PRs build fast, publishing is batched.** A PR only + smoke-tests (unit tests plus a reduced build of the changed targets), it never pushes anything. + `publish-release.yml` is the sole publisher, and each run builds a **single trigger branch** + (`main` a release, `develop` a prerelease). +- **A human merge never auto-publishes.** Publishing fires on a **`workflow_dispatch`** of + `main`/`develop` (a human-initiated release), a **code-affecting bot push to `main`** (the + codegen App merging a Dependabot/codegen PR, gated on `github.actor` so a human + merge/promotion skips it), or a **weekly `schedule`** (Docker only, to refresh the base image). + A source-only repo publishes on dispatch only. +- **The changes-detection job is a required check that must succeed, not just not fail.** A + paths-filter error must never let a target-changing PR merge with its smoke build silently + skipped. A skipped smoke job (no matching change) passes, `failure`/`cancelled` blocks. +- **Versioning is semantic and maintainer-controlled.** `version.json`'s `major.minor` is the + version floor, edited by the maintainer for functional changes only, in the PR that introduces + the work, never on a fixed cadence or mechanically after a release. NBGV appends the git height + automatically on every commit, so a release always gets a fresh build version with **no + post-release bump** and no develop-ahead requirement. +- **Docs reference the 2-digit `major.minor` line, never a 3-digit build.** `README.md`, + `HISTORY.md`, and release notes name the version as `Version 1.0` (the floor), never the concrete + build height, which is both wrong (the real height differs) and a maintenance trap. + "Correcting" `1.0` to `1.0.0` is a defect. +- **A no-op publish (unchanged NBGV `SemVer2`) re-pushes nothing to any target keyed on the + version string, except Docker, which always re-pushes** to pick up upstream base-image + refreshes. Full guarantee and the `version.json` `pathFilters` boundary: + `references/release-publish-mechanics.md`. +- **Adding, dropping, or wiring a release target** (which leaf task, which artifact-naming + contract, which seam a given output belongs to: a GitHub Release asset, a package-registry push, + an image-registry push, a filesystem deploy, or a source-only repo with no build layer at all), + and tracking an upstream release from a wrapper repo: `references/release-publish-mechanics.md`. + See also `WORKFLOW.md` for the full CI/CD contract this section's rules are load-bearing + excerpts of. + +## Operational repositories (the complete delta) + +Everything above is the `release` model. An `operational` repo (registry `workflowModel: +operational`) tracks a live service's running state rather than shipping versioned units of +delivery, and differs from the `release` model in exactly these ways, everything not listed here +stays the same: + +- **Commit configuration directly to `develop`.** There is no feature branch requirement, the + maintainer commits straight to `develop`, and only *occasionally* opens a `develop -> main` PR to + bless a known-good snapshot. The `develop` ruleset drops the PR and status-check gate, so direct + signed pushes are allowed (force-push, deletion, and unsigned commits are still blocked), and CI + runs on the push as **advisory** feedback that never rejects a commit. +- **A PR into `develop` stays available, and CI runs on it, reported but not required.** Dropping + the requirement permits the direct push, it does not withdraw the pull request, so a change worth + reviewing takes one and both paths into `develop` are legitimate. +- **Take the pull request whenever the change is not one a reader takes in at a glance and + reverts cleanly.** What decides it is the shape of the change, not a line count: restructuring + rather than adjusting a value, touching several files at once, introducing a device, an + integration, or an automation that did not exist before, and anything whose failure shows up on + the live service rather than in a lint run are each the pull request case. So is a change the + author cannot state in one sentence. This stays a judgment call by design, adding a + `pull_request` rule to the operational `develop` ruleset would gate the direct push too and + withdraw the allowance the model exists to give. +- **The `main` promotion gate is unchanged.** The shared `main` ruleset still **enforces** the + required `Check pull request workflow status job` on the `develop -> main` PR. For an operational + repo that check is lint/validation only (editorconfig/EOL plus a domain linter such as a Home + Assistant or ESPHome config validation, never unit tests), so `develop` stays the live surface + and a broken config can never reach `main`. +- **Release only by manual dispatch.** Operational repos carry `releaseTrigger: dispatch-only` and + run no codegen or auto-publish bots, publishing **only** on a manual `workflow_dispatch` (the + same source-only release the publisher already supports: tag, source zip, README, LICENSE, + NBGV-versioned), never automatically. The `develop -> main` promotion just blesses a known-good + snapshot, a release is a separate, deliberate dispatch. +- **Fleet sync still applies.** Dependabot's dual-target sync and the App-signed merge-bot run on + **every** tier, operational included, so both branches stay in sync and a promotion stays a + clean forward merge. +- **Line-ending policy differs too**, following the consuming app's native platform rather than the + fleet LF default, per the registry `lineEndings` field. That rule belongs to + `comment-and-doc-style`, not repeated here. diff --git a/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md b/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md new file mode 100644 index 00000000..6d934c0e --- /dev/null +++ b/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md @@ -0,0 +1,110 @@ +# Branch Protection and Promotion Mechanics + +Full detail for the "Branching" rules in `SKILL.md`. Load this when configuring or reconstructing +branch protection on a fleet repo, executing a `develop -> main` promotion, recovering a lost +`develop`, resolving an EOL-only promotion conflict, or working on the dual-target bot wiring +(Dependabot, codegen, the merge-bot), not for an ordinary feature-branch PR (the SKILL.md summary +covers that case). + +## Configuring branch protection: don't hand-build the rules + +Delete **all** classic branch-protection rules and stray rulesets because rulesets are the only +protection mechanism. From a hub checkout at `main`, create **exactly two rulesets named `develop` +and `main`** from the hub's `repo-config/*.json` payloads. Run +`repo-config/configure.sh apply <owner>/<repo> release|operational` from that checkout. The names +are load-bearing because governance content and workflows reference them. The registry +`workflowModel` selects the `develop` payload for a registered repository. Pass the model +explicitly for a repository outside the registry. See the hub's `repo-config/README.md` +"Rulesets" for the configured state. + +## Executing a `develop -> main` promotion safely + +Two traps, both learned the hard way: + +- **Never delete `develop`.** A promotion PR's head *is* `develop`, so `gh pr merge --delete-branch` + (and a repo's "Automatically delete head branches" toggle, kept off in the hub's + `repo-config/settings.json` for exactly this reason) deletes `develop` itself. Merge a promotion + with a plain `gh pr merge --merge`, no `--delete-branch`. If `develop` is ever lost this way, + restore it to the merged PR's head SHA, which is still reachable as the merge commit's second parent: + `gh api -X POST "repos/<owner>/<repo>/git/refs" -f ref=refs/heads/develop -f sha="$(gh pr view <n> --json headRefOid --jq .headRefOid)"`. +- **Spurious EOL-only conflicts resolve by taking `develop`.** When `develop`'s `.editorconfig` + line-ending default has changed (for example the fleet-wide CRLF-to-LF flip) while `main` hasn't + caught up yet, `develop -> main` conflicts *whole-file* on every renormalized path. + `develop`'s `required_linear_history` plus PR rulesets forbid resolving on `develop` (no merge + commit, no force-push), so resolve on a throwaway branch off `main`: + `git checkout -b promote/develop-to-main origin/main && git merge origin/develop`, take + `develop`'s side for the EOL-conflicted files (`git checkout --theirs <file>`) **after + confirming each is content-identical modulo EOL, or that `develop` is a strict superset** + (`diff <(git show ":2:<file>" | tr -d '\r') <(git show ":3:<file>" | tr -d '\r')`), then open that branch into + `main`. Verify no genuine `main`-only content is dropped (build/test where the repo supports it). + +## Why both rulesets omit "Require branches to be up to date before merging" + +The flag is off on `main` and on `develop`, for related but distinct reasons. + +- **Main**: the check is graph-based, it asks whether `main`'s tip commit is reachable from + `develop`, not whether the two branches have the same content. After any `develop -> main` + release, `main`'s tip is a brand-new merge commit that `develop`'s history doesn't contain. + Forward-only `develop` never adds it (no back-merge of `main` into `develop`), so the check + would fail on every subsequent release. Other technical workarounds (rebasing `develop` onto + `main`, or rewriting `develop`'s history) exist but contradict the squash-only `develop` ruleset + and the linearity invariant. +- **Develop**: the check stalls bot auto-merge when two bot PRs against `develop` land within the + same window. As soon as the first merges, the second flips to `mergeStateStatus: BEHIND` and + GitHub's auto-merge will not fire while strict is on. The merge-bot only *enables* auto-merge on + `opened`/`reopened` and never auto-updates bot branches, and Dependabot's rebase isn't real-time, + so the second PR sits OPEN with all checks green indefinitely. Squash mechanics still rebase the + diff onto `develop`'s tip on merge, `required_linear_history` still enforces linearity, textual + conflicts still block `mergeable: CONFLICTING`, and the required `Check pull request workflow + status job` still gates merges. The only thing lost is pre-merge detection of + *semantic-but-not-textual* conflicts, which the post-merge `develop` CI run catches anyway. + +## Dual-target bots + +**Dependabot and codegen target both `main` and `develop` in parallel.** +`.github/dependabot.yml` duplicates every ecosystem entry (one per branch) and the codegen +workflow runs as a matrix over both branches with branch names `codegen-main` and +`codegen-develop`. Each branch absorbs its own bot PRs independently, so neither falls behind, and +the forward-only rule still holds, nothing is back-merged from `main` to `develop`, both branches +receive their updates directly. The merge-bot (`.github/workflows/merge-bot-pull-request.yml`) +dispatches `--squash` or `--merge` from each PR's base ref via a `case` statement so the form +matches the ruleset on either base. Dependabot **security** PRs (CVE-driven) always open against +the repo default branch (`main`) regardless of `target-branch`, and the same `case` statement +covers them. The merge-bot auto-merges **every** Dependabot tier including semver-major (no +ecosystem or update-type guard), the required CI checks are the gate, not the bump magnitude, so a +major that breaks the build fails its checks and never merges. + +**Why parallel dual-target rather than develop-only with eventual flow-through:** +push-distribution channels (HACS for Home Assistant integrations, Linux distros that vendor from +`main`, etc.) consume `main` directly. A develop-only model would leave `main` running stale code +during long-running develop features. Codegen content can also be production-critical (live +API-derived data, language lists, build catalogs) rather than just sample/demo content, so both +branches need fresh codegen on their own cadence. + +**Maintainer-pushed commits on a bot PR auto-disable auto-merge.** The merge-bot's +`merge-dependabot` and `merge-codegen` jobs only fire on `opened`/`reopened` events (auto-merge is +enabled exactly once per PR). When a maintainer pushes commits to a bot's branch (a `synchronize` +event with an actor that isn't the same bot), the merge-bot's +`disable-auto-merge-on-maintainer-push` job fires and calls `gh pr merge --disable-auto`. The +maintainer's commits stay in the PR but won't auto-merge with the bot's content. Re-enable +auto-merge manually (`gh pr merge --auto <PR>` or the GitHub UI) when ready. + +## Codegen determinism + +The codegen workflow is a mechanism to refresh files that are checked into the repo: it runs a +matrix over `main` and `develop`, each leg regenerating against its own checkout and opening its +own PR (`codegen-main -> main`, `codegen-develop -> develop`). For the two legs not to conflict on +`develop -> main`, the generated output must depend only on its inputs, never on per-invocation +state (timestamps, GUIDs, build IDs), which would diverge every run and conflict on every release. +**What** a repo regenerates (data files, source, or both) and **how** (download and process an +external source, transform local inputs, whatever) is entirely its own concern. The constraint is +only that the output be input-deterministic, not how it is produced. A repo adopting codegen +supplies its own input-deterministic generator and wires the codegen reference workflow +(`run-codegen-pull-request-task.yml` and its scheduler). + +## App-token workflows use Client ID, not App ID + +`actions/create-github-app-token` deprecated the numeric `app-id` input in v3.0.0. Use +`client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}`. When adding new App-token call sites, use the +same form, and do not reintroduce `app-id` / `CODEGEN_APP_ID`. See the hub's +`repo-config/README.md` "Secrets" for which secrets each mechanism needs. diff --git a/.github/skills/operational-vs-release-workflow/references/release-publish-mechanics.md b/.github/skills/operational-vs-release-workflow/references/release-publish-mechanics.md new file mode 100644 index 00000000..ad7253f4 --- /dev/null +++ b/.github/skills/operational-vs-release-workflow/references/release-publish-mechanics.md @@ -0,0 +1,135 @@ +# Release Build and Publish Mechanics + +Full detail for the "Publishing" rules in `SKILL.md`. Load this when adding or removing a release +target, wiring a new leaf build task, deciding where a build output belongs (a GitHub Release +asset, a package-registry push, an image push, a deploy), or setting up a wrapper repo that tracks +an upstream release, not for reading the release model's shape (the SKILL.md summary covers that). + +## Reusable-task parameter contract + +Every `build-*-task.yml` and `build-release-task.yml` takes `ref` (git ref to check out/version), +`branch` (logical branch driving config/tags/prerelease, where `main` => Release/`latest`/ +non-prerelease, else Debug/`develop`/prerelease), and where relevant `smoke`. +**Branch-derived config keys off `inputs.branch`**: each run builds one branch, and the top-level +publisher passes `branch: ${{ github.ref_name }}`, which the tasks forward and read as +`inputs.branch` (not `github.ref_name`) for config/tags/prerelease. `get-version-task.yml` takes a +`ref` so NBGV versions the right branch. + +## Per-target subsetting + +`build-release-task.yml` is a hub-hosted task with per-target `enable_*` inputs, so a repo drops a +target by setting its `enable_<target>: false` at the caller stub rather than deleting a job: the +hub task carries the full job graph for every repo, and the caller stub's `with:` block is where +the target list is expressed. A repo still curates its path-filter entry in +`test-pull-request.yml`, and (for PyPI) the `publish-pypi` job in its own `publish-release.yml`, +since `id-token: write` belongs at that one entry point. CodeGen, versioning, badge, merge-bot, +and Dependabot are target-agnostic. + +## Orchestration vs. build: the override seam + +The pipeline splits into two layers. The **orchestration** layer is generic and is the +standardization baseline: `publish-release.yml` (single-branch publish plan), the `get-version` +task plus `github-release` job inside `build-release-task.yml`, `get-version-task.yml`, and the +aggregator shape of `test-pull-request.yml`. Within +`test-pull-request.yml`, only the `changes -> smoke-build -> check-workflow-status` aggregator +wiring and the ruleset-bound job name are verbatim orchestration, while the `unit-test` job and +the `dorny/paths-filter` entries are owned/per-target. The **build** layer is a hook: a composite +action at `.github/actions/build-<target>` the hub-hosted `build-release-task.yml` reaches. The +hub defaults require explicit project paths. A project needing more than a path override carries +its own hook. + +The contract that keeps the seam clean: **a target contributes files to the GitHub release by +uploading a workflow artifact named `release-asset-<branch>-<target>`.** The `github-release` job +collects every `release-asset-<branch>-*` artifact by pattern, so its `download-artifact` step +uses `pattern:`/`merge-multiple:`, **never an `artifact-ids:` that names a build job's output** +(the producing build jobs still appear in `needs` for sequencing). That makes the tag-the-commit +plus create-the-release plus attach-the-assets logic reusable **as-is** across repos. **This +name-pattern handoff is canonical for every repo, single-target included**: name your one asset +`release-asset-<branch>-<target>` and the verbatim `github-release` globs it. Do not switch a +single-target repo to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which +looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. + +**What a repo still curates** (by design, not a leak): which `enable_<target>` inputs its caller +stub sets, per the per-target subsetting rule above. `build-release-task.yml` is hub-hosted +(`docs/reusable-workflows.md` "Stage 4: The Release Chain and the Docker Core"), so its job graph +and its `github-release` job are the hub's, not a per-repo file a caller edits. A repo adopting the +release chain carries only the caller stub in its own `publish-release.yml` and +`test-pull-request.yml`, naming the hub task by pin and setting the `enable_*`, `docker_image`, +and project-path inputs its targets need. + +## Map your outputs to the right seam + +Pick by where each artifact *goes*, not by language: + +- **Files attached to the GitHub Release** (zips, binaries, packaged libraries): a dotnet-publish + hook or a build-nuget hook per output, each uploading `release-asset-<branch>-<name>`. This is where the + .NET `dotnet publish` or `dotnet build` and package push lives. The hub default takes an explicit + project path, and a project needing different build behavior replaces the hook. A data-only + repo's own output (e.g. a symbol library) is not yet + expressible as a hub hook or an `enable_*` input, so it stays a carried leaf until the hub task + grows one. +- **Package-registry pushes** (NuGet.org, PyPI): the target both builds **and** publishes to its + registry. NuGet pushes from inside the build-nuget hook (OIDC trusted publishing through + `NuGet/login`, no stored API key) *and* also uploads a `release-asset-*` (.7z) for the GitHub + release. PyPI is split: the build-pypi hook only builds and uploads the + `pypi-build-<branch>` artifact, and the separate `publish-pypi` job in the caller's own + `publish-release.yml` does the OIDC Trusted-Publishing upload (`id-token: write` is granted only + at that one entry point), and PyPI contributes **no** `release-asset-*`. +- **Image-registry pushes** (Docker Hub): `build-docker-task.yml`, hub-hosted like + `build-release-task.yml`, pushes multi-arch tags directly and contributes **no** + `release-asset-*`. The image set comes from a docker-prepare hook (the hub default emits the + single vanilla entry an `image` input implies). A multi-image or upstream-pinned repo carries its + own hook, and a shared base layer comes from a required docker-build-base hook with no hub + default. To publish the Docker Hub repository overview, the hub-hosted `publish-docker-readme-task.yml` + pushes a readme via `peter-evans/dockerhub-description` (single-repo by default, matrix per + image for multi-image repos), wired into `publish-release.yml` and gated to `main` both by the + caller's `branch` input and inside the task itself. A `docker-readme-transform` hook sets a + `readme-filepath` step output naming which file to push, defaulting to `Docker/README.md` if + present else `README.md` as-is, so a repo needs a hook only to render the file first or to + override that default. +- **Filesystem on a host the project owns** (a static site, a config tree): a deploy leaf builds + the tree and ships it over the repo's own transport, contributing **no** `release-asset-*`. It + is a **separate `workflow_dispatch`** from the release, so a redeploy of an unchanged commit + mints no tag, and its credentials come from a **per-environment GitHub Environment** rather than + the repository secret store. Its last step asserts what the host actually serves, the release id + and the environment, never that the transport exited zero. Retention at the destination is + bounded by a declared count, and one side is recorded as owning the prune: the deploy where its + credential can observe the destination, the host where that credential is deliberately + write-only. +- **Source-only / no build** (validate + tag + release): the repo has no leaf build tasks. + Its dispatch-only `publish-release.yml` calls the hub-hosted `build-release-task.yml` after the repo's reusable validation task succeeds. + The caller sets `github: true`, every `enable_*` input to false, and `expect_release_assets: false`. + The reusable task runs NBGV and creates the release with the tag, automatic source archive, README, and LICENSE. + +`get-version-task.yml` installs the .NET SDK only because NBGV needs the runtime to compute the +version/tag, which is heavyweight but expected even for a non-.NET repo, and acceptable as-is. + +## No-op republish guarantee + +A weekly/dispatch publish where NBGV `SemVer2` is **unchanged** (no new commit since the last +publish) re-pushes **nothing** to GitHub Releases (the `github-release` job's `release-exists` +check skips the create step), NuGet (`dotnet nuget push --skip-duplicate`), or PyPI +(`gh-action-pypi-publish` `skip-existing: true`), since all three key on the version string. +**Docker always re-pushes** by design: it picks up upstream base-image refreshes (e.g. +`ubuntu:rolling`) that aren't visible in the repo. Boundary: `version.json` has **no +`pathFilters`**, so *any* commit, including a CI/workflow-only or docs-only change, advances the +NBGV git height and therefore `SemVer2`, and the next publish *does* create a fresh release for it +even when the shipped binary is byte-identical. This is accepted NBGV behavior, and `pathFilters` +are intentionally not added. + +## Wrapper repos that track an upstream release + +A repo wrapping an upstream release uses the hub-hosted `check-upstream-version-task.yml`: a +required `resolve-upstream` hook sets a `versions` step output, a **JSON object of +`name -> version`**, written to a committed state file at the **repo root beside `version.json`** +(default `upstream-version.json`, since it is a build-input version source, not GitHub-platform +config, so it does not belong under `.github/`), and opens a rolling App-signed bump PR per branch +that the merge-bot auto-merges (`merge-upstream-version`). The object carries one key for the +common single-version case (`{"version": "X"}`) or N keys for a wrapper that pins several upstream +components (e.g. an image plus a companion tool), and the build reads each component by key, and +the bump PR's title/body name only the keys that actually moved. Call it from a scheduled +entry-point workflow and matrix only the branches that ship the version (a CI-only version uses +`["develop"]`). A merged bump ships on the **next publish**, not immediately, which is the +two-phase latency tradeoff. A tracker whose bump needs a human decision instead of auto-merge, for +example one that snapshots a package list to review rather than a version to adopt outright, sets +`auto-merge: false`, which prefixes the head so no merge-bot rule matches it. diff --git a/.github/skills/pr-review-conduct/SKILL.md b/.github/skills/pr-review-conduct/SKILL.md new file mode 100644 index 00000000..810955eb --- /dev/null +++ b/.github/skills/pr-review-conduct/SKILL.md @@ -0,0 +1,178 @@ +--- +name: pr-review-conduct +description: >- + Governs opening, driving, and merging a pull request review loop in a ptr727/ProjectTemplate + fleet repo: requesting a review after a push, triaging findings (including suppressed + low-confidence ones), replying and resolving threads, and deciding whether a PR is actually + mergeable. Use this whenever about to open a PR, immediately after creating one, about to merge + a PR, enable auto-merge, ask the maintainer for merge permission, push a fix and move on without + re-checking review state, or judge a PR "green" or "clean" from CI or mergeStateStatus alone. + Triggers even when the request sounds routine, such as "open a PR," "merge this," or "it's all + green, go ahead," because PR creation starts the review loop and mergeStateStatus: CLEAN + can go clean once checks pass and every known thread is resolved, while still saying nothing + about whether the review that resolved those threads covered the current head SHA, read the + full diff, or left a suppressed low-confidence finding, which opens no thread at all, + unanswered. Also triggers when a review loop looks stuck + (no review landing, findings that keep reappearing) or when deciding a finding is real, false, + deferred, or a deliberate decline. Provider-specific mechanics are implemented by + scripts/pr_review.py and bootstrapped by .github/copilot-instructions.md. This skill is the + contract those surfaces implement, not a replacement for them. +--- + +# PR Review Conduct + +## Why this exists + +`mergeStateStatus: CLEAN` reflects required status checks and any review thread the ruleset's +conversation-resolution requirement already tracks as resolved. It says nothing about whether the +review that resolved those threads actually covered the **current** head SHA, whether it read the +full diff rather than part of it, or whether a suppressed low-confidence finding, which never +opens a thread for the ruleset to see, was ever answered. A PR that looks done, green checks, no +visible comments, routinely still carries a finding nobody has answered. Treating "green" as +"mergeable" is the single most common way this loop gets skipped. + +## Merge Gate, check this before merging or enabling auto-merge + +**Do not merge, and do not enable auto-merge, unless ALL of these hold:** + +1. Required status checks are green, and where they are not, the reason is **read**, never + inferred. `BLOCKED` covers a failed check, a required check nothing is running, an unresolved + thread, and a missing approval alike, and the response differs by cause. +2. A review is confirmed on the **current head SHA**, matched by commit SHA rather than assumed + from a green merge-state. A push makes checks go green *before* the re-review lands, and the + matched review is **read**, not just counted. A review can carry the head SHA and still decline + the PR outright, or say it read only part of the changed files. `pr_review.py`'s + `review_on_head` names Copilot's own coverage specifically, the currently required reviewer, + not "no review of any kind covers this head": a trialed advisory reviewer (CodeRabbit, + Qodo) carrying the exact head under `other_reviewed`, with an empty review body and no new + threads, is its own ordinary "reviewed, nothing to flag" shape, not a missing review (#1066). +3. **Every** finding on that head SHA is closed: threads resolved, issue-level comments (which + have no resolve action) triaged and replied to, **and** the low-confidence findings collapsed + in the review body investigated and answered. Those appear in no thread, so polling threads + alone reports a clean pass while they stand. The same holds for CodeRabbit's own + "outside diff range" comments (`cr_outside_diff` in `pr_review.py`'s digest) and for Qodo's + comment-only findings (`qodo_open`): neither opens a `reviewThreads` entry either, so + give each one the same triage the low-confidence findings above already get (#1058). Qodo's own + `Resolved`/`Dismissed` self-tracked badge is a fast pre-triage signal, not a substitute for + reading the finding, spot-verify against `gh pr diff` rather than trusting it outright. +4. Nothing in the review was a shape the tooling could not read (an unrecognized heading, a moved + section, an unfamiliar coverage wording). An unrecognized shape blocks the gate on its own. + File an issue naming it and quoting the body, rather than guessing what the new wording + probably meant. +5. The maintainer has given **explicit** permission to merge. + +The agent never merges on its own. A green or CLEAN PR with one open finding is not mergeable, +full stop, whatever the merge-state field says. + +## Expected review loop + +Open every fleet-owned pull request ready for review. Draft state delays the loop and causes +reviewers to skip, so it has no place in the internal feature-to-develop or develop-to-main +workflow. The separately documented `upstream-contribution-workflow` may use a draft while a +third-party contribution is still being prepared for upstream review. + +Opening a pull request starts this loop by default. Creating the PR is not a terminal handoff. +Only an explicit maintainer instruction may stop, defer, or alter the loop. Silence or a request +that says only "open a PR" is not such an instruction. + +Run every `scripts/pr_review.py` command below from a hub checkout. The script is hosted there and +is never carried into a downstream repository. + +Run `local-strict-review` against the branch's current diff before step 1's push, and again before any fix push under outcome 1 below. + +1. Push changes to the PR branch and open the pull request when it does not exist. +2. Run `scripts/pr_review.py status` once in the foreground and read its output. +3. Re-request a review for the **current head SHA**. Auto-trigger is unreliable, so request it + explicitly (mechanics in the Copilot runbook). The UI is a fallback only. +4. Run a bounded `scripts/pr_review.py wait` in a background process and read its terminal output. + A completed review raising **no findings** is a valid terminal outcome, so do not re-trigger it + or read silence as a missing review. A review whose body says it declined to review is the one + exception, and it is terminal the other way. Nothing follows it, and re-requesting the same + head only repeats the decline. +5. Triage findings (see below). +6. Apply fixes or write a rationale for declines. +7. Reply to each thread and resolve what was addressed. +8. Re-run the loop after every fix push until the checks are green and no finding remains open. + +The review effort setting is user-controlled. The workflow never selects or changes it. `status` reports `Lite`, `Balanced`, or `Max` when the completed review exposes that metadata, and distinguishes an inherited `Default (<level>)` from an explicit choice. Missing effort metadata reports `unknown` and does not change coverage or completion. A pending effort-labeled request can complete without a `copilot_work_started` timeline event, so absence of that event never proves the request is abandoned. The bounded timeout reports `PENDING` when no review or terminal answer arrives. After a timeout with `requested=yes`, rerun `wait` for another bounded interval by default because the request may still be active. If the maintainer directs a retry, remove Copilot in the pull request UI, add it again, and rerun `wait`. This recovery replaces only the review request and never changes the effort setting. + +Drive to green, a review confirmed on the latest head SHA and every actionable finding closed, +then apply the Merge Gate above. **Never exit the loop early.** A round count is not a stopping +condition, and neither is patience running out. Reporting only that the PR was opened is an early +exit unless the maintainer explicitly instructed the agent not to monitor or drive its review. + +After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. + +## Every finding ends in one of five outcomes + +1. **Real, so fix it.** Run `local-strict-review` against the branch's current diff before pushing + the fix, then reply with the fixing commit SHA. For a finding on platform-specific code + (PowerShell, a macOS- or WSL-only path), "fixed" means executed on that platform, per + `agent-conduct` "Before Claiming Done": a fix reasoned out by analogy to a tested equivalent + elsewhere is not yet fixed, and the reply says so rather than claiming the SHA closes it. +2. **Not real, or real but structurally out of scope, so decline in the thread with evidence.** + Disprove a wrong finding with the command and its output, the code path that makes it + impossible, or the rule that governs it. A finding that is factually correct but not this + repo's to fix (a verbatim-fidelity manifest entry byte-locking the section, ownership that + sits elsewhere) declines the same way: name the boundary and cite what proves it. Either shape + closes the thread on its own evidence. An assertion ("this is fine") does not close a finding, + a decline needs evidence the reviewer itself could check. +3. **Real, fixable here, but deliberately left as is, a value call rather than a scope + boundary, so it is the maintainer's, not the agent's.** Reach for this only once outcome 2 is + ruled out, since a scope boundary declines on its own evidence and never needs this outcome at + all. State the finding and why the fix is unwanted, and get an explicit answer in the same + turn, before moving to other work. A plan to ask later is resolution by silence the moment + attention moves elsewhere. If the maintainer is not reachable right now, leave the thread open + and say so, rather than treating the intention to ask as the asking. +4. **Real and worth doing later, so file the issue first, then reply with its link.** A deferral + noted only in a thread is lost the moment the PR merges. +5. **Keeps recurring, so fix the class, not the instance.** A finding raised repeatedly against + correct code means the code is not communicating something: add the comment, sharpen the name, + narrow the interface, or fix the rule if the rule is wrong. Bouncing the same point across + rounds is the signal to escalate the rule itself, not to keep re-arguing it. + +**A disposition decided on one PR does not carry to the next.** The same finding shape recurring +on a sibling repo or PR, even within one batch or one session, gets its own outcome: its own +evidence-backed decline (outcome 2) or its own explicit maintainer answer (outcome 3). A prior +instance's outcome is context for the new one, never a standing answer to reuse in its place. + +## Triaging findings + +**A low-confidence (suppressed) finding is not a low-value one.** Judge each against the code, +never against its confidence label. Classify before responding: + +- **Bug**, wrong behavior, missing coverage, a real code or doc divergence. Fix it. +- **Style or convention**. If the cited rule matches the existing tree, fix the code. If the rule + contradicts the tree or industry norm, **fix the rule, not the code**, and take it to the + maintainer (outcome 5) rather than bouncing the same code across rounds. +- **Architectural opinion**, a proposed redesign. Surface it with a recommendation, never apply + it unilaterally. + +## Answering a suppressed finding + +A suppressed finding has no thread and no resolved or unresolved state, so an answer needs to +carry its own context: quote the finding (with its `file:line` anchor and enough of the +reviewer's own words to identify it), give one bold verdict per finding (`Fixed in <SHA>`, +`Disproven`, or `No change needed`), state the `(N)` count the block gave so answers can be +checked against findings, and link the review round. **Read every round, not only the head.** A +suppressed finding does not retire when a later push supersedes it, it just stops showing up in a +head-scoped query while still unanswered. Post the answer with `scripts/pr_review.py comment` +from a hub checkout. Do not use a provider connector or reconstruct the GitHub mutation. + +## Escalate to the maintainer when + +- A genuine design trade-off surfaces (fail-open vs. fail-closed, refactor scope). +- A finding keeps recurring. Bring the pattern and a recommended fix (rule change or code + change), don't keep silently re-declining it. +- A finding is judged real but should not be fixed. That decision is never the agent's alone. +- An architectural redesign is proposed rather than a bug fix. + +## Mechanics Live Elsewhere + +This skill is the provider-agnostic contract. Use `scripts/pr_review.py` from a hub checkout for +the GitHub-specific API operations. `status` reports coverage, threads, body-only findings, and +shapes in one call. `wait` requests and polls in-process. `comment` posts a PR-conversation +answer after it reads the PR node ID. `reply` resolves a thread by matching the finding's own +words instead of a line number a fix push can move. The repository's +`.github/copilot-instructions.md` bootstraps Copilot into the `code-review` skill and its stable +coverage marker. Do not reconstruct the API operations by hand. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md new file mode 100644 index 00000000..f7a7451e --- /dev/null +++ b/.github/skills/python-codestyle/SKILL.md @@ -0,0 +1,185 @@ +--- +name: python-codestyle +description: >- + Governs Python code style for ptr727/ProjectTemplate fleet repos: the build-versus-lint-only + profile split, the uv/ruff/pyright/mypy/pytest toolchain, src layout, formatting and linting, + comment and docstring conventions, type hints, naming, imports, patterns to avoid, test + conventions, and versioning. Use this whenever writing, reviewing, or editing a .py file, a + pyproject.toml, or a uv.lock, whenever running or choosing a Python formatting, lint, type-check, + or test command, whenever deciding whether a Python subtree is a shippable project or a lint-only + scripts tree, whenever choosing pyright versus mypy for a repo's CI gate, or whenever writing or + reviewing a Python test. Triggers even when the task looks like a small local fix ("just add a + helper function", "silence this lint warning", "add a dependency") or verification step ("run + the tests"), because choosing pytest before reading the profile turns an intentional unittest + suite into a false missing-dependency diagnosis. Applies only to a repo's Python side, a repo + with no Python has no use for this Skill. +--- + +# Python Codestyle + +## Why this exists + +This is the Python-specific half of the fleet's code style guide, kept in one place instead of +re-derived per repo or per session. CODESTYLE.md's General section still owns the rules every +language shares (clean-compile verification as a concept, the suppression-scope order, tooling +casing in prose), this Skill is everything specific to a Python project on top of that: the two +profiles, the toolchain, layout, and the language-level conventions. + +## Two profiles + +Read the repo's `OPERATIONS.md` local-verification commands before substituting a generic command. +Then read the `pyproject.toml` shape and pick the profile before running Python tooling or tests: + +- **build** (Project): `[project]` + `[build-system]` + committed `uv.lock`. Uses `uv run`, pytest, + pyright strict (or mypy where the repo requires it). +- **lint-only** (Scripts): no `[project]`, no lockfile. Uses `uvx` for third-party tools, unittest + for tests, and mypy as the CI gate. Do not run pytest or diagnose its absence as an environment + defect. Use the repository's exact coverage command and unittest scope from `OPERATIONS.md`. + +For the full profile specification and per-repo adaptation axes (type checker, dependency +declaration, versioning, VS Code config), see `references/profiles.md`. + +## Toolchain + +| Tool | Role | Config | +|---|---|---| +| [uv][uv-link] | env, deps, build, publish (build/publish only where the repo ships a package) | `pyproject.toml` `[dependency-groups]` or `[project.optional-dependencies]`, `uv.lock` | +| [hatchling][latest-link] | build backend (published packages) | `pyproject.toml` `[build-system]` | +| [ruff][ruff-link] | lint + format + import sort | `pyproject.toml` `[tool.ruff]` | +| [pyright][pyright-link] | type checker (the default, a strict baseline) | `pyproject.toml` `[tool.pyright]` | +| [mypy][mypy-link] | additional/alternate type checker (optional, the CI checker in a mypy-in-CI repo, required for Home Assistant) | `pyproject.toml` `[tool.mypy]` (or per home-assistant/core) | +| [pytest][docs-link] | test runner (build profile only, lint-only uses `unittest`) | `pyproject.toml` `[tool.pytest.ini_options]` | + +**Type checking targets strongly typed, deterministic code.** pyright in strict mode is the +default baseline on first-party code (a repo may instead run mypy in CI and keep pyright +editor-only via Pylance, per the next paragraph): `[tool.pyright]` `strict = ["src"]`, or the +integration package for a Home Assistant repo, with tests run in standard mode. pyright is the +anchor because Pylance embeds it, so the editor and the CLI/CI (`uv run pyright`) run the same +engine and never disagree. The standalone `ms-pyright.pyright` extension stays in +`unwantedRecommendations` because Pylance covers it. Relax strictness on third-party code only +when a dependency has no usable types and no alternative (e.g. `pandas`): a targeted, commented +`# pyright: ignore[...]` or a scoped `[tool.pyright]` override, never a blanket relaxation. + +**mypy is allowed, and required where the ecosystem demands it, it is not banned.** Running more +than one checker is normal when each serves a purpose (the .NET side pairs CSharpier and +`dotnet format` the same way), and pyright's inference and mypy's plugin ecosystem (e.g. +`pydantic.mypy`) catch different classes of error. A Home Assistant integration runs +`mypy --strict` because the platinum `strict-typing` quality-scale tier requires it, and a +pydantic-heavy library may opt in for the plugin. When a repo uses mypy it runs in CI and the +editor (the `ms-python.mypy-type-checker` extension) so the two stay consistent, and its mypy +command joins the clean-compile. A repo with no such need stays pyright-only, which is lighter and +inherently consistent. + +## Local development loop + +From inside a **build**-profile Python project directory. A **lint-only** Scripts profile has no +`uv.lock` to sync and no pytest to run, substitute `uvx` per tool and `unittest` per the Two +Profiles section above: + +```sh +uv sync # creates .venv, installs deps + dev group +uv run ruff format # auto-format +uv run ruff check --fix # auto-fix lint +uv run ruff check # verify lint clean +uv run ruff format --check # verify format clean +uv run pyright # verify types +uv run pytest # run tests +uv build # produce wheel + sdist in ./dist (published packages only) +``` + +The **build**-profile Python clean-compile is `uv run ruff format` + `uv run ruff check` + the +repo's type checker: `uv run pyright`, or `uv run mypy src` where mypy is the CI checker, or both +where the repo runs both (see Type checking above). Run it, plus `uv run pytest`, before +committing. A **lint-only** profile's clean-compile substitutes its `uvx` and `unittest` +equivalents, per Two Profiles above, and has no such command to run before committing beyond +those. These are documented commands, and an optional VS Code tasks mirror (all `type: process`, +no `&&` shell chaining, so it runs the same on any task shell) is in the hub +`vscode-tasks-python.json` snippet. CI runs the same clean-compile commands as the authoritative +backstop. A working local hook is strongly suggested, not opt-in: wire the Python `pre-commit` +framework from the canonical `catalog/snippets/pre-commit/.pre-commit-config.yaml`. See +GOVERNANCE.md "Running the Linters Locally" for what the hook must cover and what its absence +means. + +A restricted executor gives each task a cache directory under a writable temporary root. Point +`UV_CACHE_DIR`, `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR`, and `COVERAGE_FILE` into that directory before +running the applicable tools. This keeps their generated state outside both the home directory +and the checkout. Do not change `HOME` or an agent configuration directory. A denied network +request means the tool did not run, so preserve the denial and rerun through the executor's scoped +approval mechanism. + +## Layout + +`src` layout, which keeps the package out of the repo root and prevents accidental imports of +unbuilt code: + +```text +<python-project>/ + pyproject.toml + README.md + uv.lock # committed for reproducible CI + src/ + <package_name>/ + __init__.py + _version.py # published packages; a source-only repo uses a static version instead + <modules>.py + tests/ + __init__.py + test_<module>.py +``` + +## Code style + +Key rules for every Python task: + +- **`ruff format` is authoritative.** Don't argue with the formatter. Configure in `pyproject.toml` + `[tool.ruff]`, not via inline `# fmt:` directives. +- **Run `ruff check --fix` before committing.** The configured rule families are in + `[tool.ruff.lint]` `select`. Add new rule families project-wide, not scattered inline `# noqa`. +- **`# noqa` is a last resort.** Scope it narrowly (`# noqa: E501`) with a comment. Recurring + false positives belong in `[tool.ruff.lint]` `ignore` or `per-file-ignores`. +- **All public APIs are typed.** Use modern syntax (`list[int]`, `X | None`). Don't add + `# type: ignore` without an explaining comment. +- **Don't add backward-compat shims.** Just delete unused code. Git history is the audit trail. +- **Don't add error handling for impossible cases.** Trust internal code. Validate only at boundaries. + +For comments, docstrings, full type-hint rules, naming, imports, and all patterns to avoid, see +`references/code-style.md`. + +## Tests + +`uv run pytest` for a build profile, `unittest` for a lint-only Scripts profile (see Two Profiles +above). One test file per module (`test_<module>.py`). A build profile prefers fixtures over +`unittest`'s `setUp`/`tearDown` lifecycle hooks. A lint-only profile uses those hooks directly, +since `unittest` has no fixture-injection mechanism of its own. Fakes over mocks either way. Test +the docstring's contract, not implementation details. See `references/testing.md` for the full +build-profile conventions, and `references/profiles.md` for the lint-only `unittest` conventions. + +## Versioning + +Published packages use `_version.py` with `__version__ = "0.0.0"` as a placeholder. Wire +`hatch-vcs` or equivalent to increment, publish with `skip-existing: true`. Source-only repos use +a static `version` in `[project]` with no `_version.py`. See `references/profiles.md` for details. + +## Linter cleanliness + +Before pushing or opening a PR: + +- VS Code's Problems pane should be quiet for the files you touched. The relevant linters are ruff + (via the `charliermarsh.ruff` extension) and pyright (via the `ms-python.python` extension's + bundled Pylance). +- The **build**-profile CI gate is `uv run ruff check`, `uv run ruff format --check`, the repo's + type checker (`uv run pyright` or `uv run mypy src`), and `uv run pytest`, the same commands as + the local loop above, run from the Python project directory (invoked as separate steps, not + `&&`-chained, so the runner shell is irrelevant). A **lint-only** profile's CI gate is its `uvx` + equivalents plus its `unittest` suite, per `references/profiles.md`. +- Markdown in this directory follows CODESTYLE.md's repo-wide Markdown and Spelling rules, + packaged as the `comment-and-doc-style` Skill. + +<!-- External --> + +[docs-link]: https://docs.pytest.org/ +[latest-link]: https://hatch.pypa.io/latest/ +[mypy-link]: https://mypy-lang.org/ +[pyright-link]: https://microsoft.github.io/pyright/ +[ruff-link]: https://docs.astral.sh/ruff/ +[uv-link]: https://docs.astral.sh/uv/ diff --git a/.github/skills/python-codestyle/references/code-style.md b/.github/skills/python-codestyle/references/code-style.md new file mode 100644 index 00000000..9ad17f29 --- /dev/null +++ b/.github/skills/python-codestyle/references/code-style.md @@ -0,0 +1,93 @@ +# Python Code Style: Full Reference + +## Formatting and linting + +- **`ruff format` is authoritative.** Don't argue with the formatter, and if it reformats your + code, that's the final form. Configure (line length, target version) in `pyproject.toml` + `[tool.ruff]`, not via inline `# fmt:` directives. +- **Run `ruff check --fix` before committing.** Most ruff lint rules have safe autofixes, let the + tool handle them. The configured rule families are listed under `[tool.ruff.lint]` `select`. Add + new rule families project-wide rather than scattering inline `# noqa` markers. +- **`# noqa` is a last resort.** When you must use one, scope it narrowly (`# noqa: E501`, not + bare `# noqa`) and add a short comment on the same line explaining why. False-positive patterns + that recur across the codebase belong in `[tool.ruff.lint]` `ignore` or per-file + `[tool.ruff.lint.per-file-ignores]`, with a comment. Porting an existing codebase is not a + license to add `ignore` / `per-file-ignores` blocks to mute newly surfaced lint. Fix it. + +## Comments + +- **Inline `#` comments**: keep tight and local. One line is preferred, but multi-line is fine + when you need to document a non-obvious implementation constraint, a local trade-off, or + coupling that future edits could easily break. Keep that rationale next to the affected block so + the reviewer/maintainer sees it at edit-time. +- **Don't explain what the code does.** Well-named identifiers handle that. Don't reference the + current task ("added for X", "used by Y"), which belongs in the PR description. + +## Docstrings + +- Follow [PEP 257][pep-0257-link]. Focus docstrings primarily on the behavior contract (what + callers and tests can rely on), public semantics, and edge-case expectations. + Implementation-local rationale belongs in inline `#` comments, not docstrings. +- A short one-liner is fine for trivial functions and tests with self-documenting names. +- For non-trivial behavior (non-obvious test scenarios, contracts a test pins, edge cases callers + must know about, design trade-offs that are load-bearing for future maintainers), write a + one-line summary, blank line, then a details paragraph. Multi-paragraph docstrings are fine when + the contract earns it. +- Design notes belong in the code (docstrings or inline comments). They do NOT belong in + `HISTORY.md`, which is end-user release notes, not a design log. + +## Type hints + +- **All public APIs are typed.** The repo's configured type checker runs on `src/` (pyright strict + via `[tool.pyright]` `strict = ["src"]`, or mypy where that is the CI checker), and tests run in + the checker's looser/standard mode. +- **Use modern syntax**: `list[int]` not `List[int]`, `dict[str, X]` not `Dict[str, X]`, + `X | None` not `Optional[X]`, `from __future__ import annotations` only when needed for forward + references. +- **Don't hedge that syntax for an older interpreter.** `pyproject.toml` pins `target-version` / + `python_version` to 3.13 for every Python profile in this repo, and `spec/host-tools.json` + carries that as the host floor `scripts/host_gate.py` enforces, so 3.10+-only syntax (`X | None`, + `match`, etc.) needs no quoting, no `typing.Union` fallback, and no `from __future__ import + annotations` guard on that account alone. Add that import only when a real forward reference + needs it, per the bullet above. Three named exceptions carry a lower floor on purpose and say so + themselves: `scripts/skills_install.sh` and the `install-skills.*` bootstrap scripts, which must + run on whatever interpreter a host already has before this floor's toolchain exists to install + one, and `spec/resolve_description.py`, which `repo-config/configure.sh`'s own bootstrap probe + accepts down to 3.7 for the same reason, and which carries `from __future__ import annotations` + for exactly that purpose rather than out of habit. No other `spec/` code has a reason to hedge, + so that import or a quoted annotation appearing anywhere else in `spec/` is a sign this one + exception got copied rather than a pattern to follow. +- **Don't add `# type: ignore` to silence pyright errors without a comment** explaining the + constraint. If a recurring false positive needs suppression, configure it project-wide in + `[tool.pyright]`. A new port doesn't change this, fix freshly surfaced type errors rather than + muting them. + +## Naming + +- `snake_case` for functions, methods, variables, modules, package directories. +- `PascalCase` for classes, type aliases, type vars, enum members. +- `UPPER_SNAKE_CASE` for module-level constants. +- Single leading underscore for module-private, double leading underscore for name-mangled (rare, + and usually means rethink the design). + +## Imports + +- **Let ruff sort imports.** `[tool.ruff.lint]` `select` includes the `I` rule family + (isort-equivalent). Don't hand-sort. +- Standard library first, then third-party, then first-party (the project itself), each block + separated by a blank line, which ruff enforces automatically. +- Avoid wildcard imports (`from x import *`) outside `__init__.py` re-exports. + +## Patterns to avoid + +- **Don't add backward-compat shims, `# removed` markers, or rename-to-`_` for unused vars**, just + delete. Git history is the audit trail. +- **Don't add error handling for impossible cases.** Trust internal code, and validate only at + boundaries (user input, parsed config, external APIs). +- **Don't use exceptions for expected control flow.** Exceptions are for unexpected states. +- **Don't suppress errors silently** (`except Exception: pass`). Either handle the specific + exception and document why it's safe, or let it propagate. + +<!-- External --> + +[pep-0257-link]: https://peps.python.org/pep-0257/ diff --git a/.github/skills/python-codestyle/references/profiles.md b/.github/skills/python-codestyle/references/profiles.md new file mode 100644 index 00000000..b7eedfb9 --- /dev/null +++ b/.github/skills/python-codestyle/references/profiles.md @@ -0,0 +1,74 @@ +# Python Profile Details + +## Adapt before propagating + +The rules in `SKILL.md` describe the default Python profile: a package that publishes to PyPI, +type-checked by pyright in strict mode, dependencies in `[dependency-groups]`. A derived repo +often differs, and when it does, adapt these fields to match the repo's actual toolchain rather +than copying verbatim (a verbatim copy that misdescribes the repo is inaccurate and gets rejected +in review). The axes that commonly vary per repo: + +- **Type checker in CI**: pyright strict, mypy in CI with pyright editor-only (Pylance), or both. + Whichever runs in CI is the one the clean-compile and the CI gate invoke. +- **Dependency declaration**: `[dependency-groups]`, or PEP 621 `[project.optional-dependencies]` + (dev tools installed with `uv sync --extra <group>`). +- **Versioning / publishing**: a published package (`_version.py` plus a version source, + `uv build`, and a PyPI publish step), or a source-only repo with a static `version` and no + publish step (see Versioning below). +- **Disabled markdownlint rules**: repo-specific, `.markdownlint-cli2.jsonc` at the repo root is + the source of truth, not any example rule named here. +- **VS Code config home**: editor settings/extensions may live in `.vscode/*.json` or the + `<Repo>.code-workspace`, while tasks/launch/debug configs can only be external `.vscode/*.json` + (they cannot live in the workspace file). The repo's own `tasks.json` sits wherever it keeps it, + and the canonical task definitions it is written against are the hub `vscode-tasks-python.json` + snippet, which resolves the same way from every repo. + +## Two profiles: full specification + +A repo's Python is one of two shapes, declared as the `build` or `lint-only` profile and validated +against the `pyproject.toml` shape. Most of the `SKILL.md` rules (uv project, `uv.lock`, `uv run`, +src layout, pytest coverage) describe the Project shape (the `build` profile). The two differ by +whether the Python has third-party runtime dependencies, which shows up structurally in +`pyproject.toml`, so the fleet's audit reads the shape there: + +- **Project** (the `build` profile): the Python has third-party runtime dependencies, or is the + repo's deliverable. It is a PEP 621 uv project: `[project]` with `dependencies` (dev tools in + `[project.optional-dependencies]` or `[dependency-groups]`), a `[build-system]`, and a committed + `uv.lock` (pinned LF, per GOVERNANCE.md's "Line Endings" section). CI runs `uv sync --frozen` + + `uv run <tool>`, so the lockfile pins tool versions. +- **Scripts** (the `lint-only` profile): stdlib-only utility scripts embedded in a non-Python repo + (e.g. a Python tooling subtree of a `csharp` app). Run the tools with `uvx` (no project install, + no lockfile): the `pyproject.toml` carries only tool config (`[tool.ruff]`, `[tool.mypy]`, and + an optional `[tool.pyright]` editor block), with no `[project]`, no `[build-system]`, and no + `uv.lock` (that metadata would misrepresent it as a shippable package). mypy is the type-check + gate (there is no first-party package for pyright strict to anchor on), and a `[tool.pyright]` + block in standard mode keeps Pylance quiet in the editor, the same mypy-gate/pyright-editor + split the build profile uses. There is no lockfile, and a `uvx <tool>@<ver>` pin in a `run:` + step is not something Dependabot tracks, so CI runs `uvx ruff@latest` / `uvx mypy@latest` rather + than a manual pin that would silently go stale. The fleet rule is to pin only what Dependabot + auto-updates (SHA-pinned actions, package deps) and otherwise run latest, so the VS Code tasks, + README, and CI all run the unpinned latest here. `.py` files follow the repo's LF line-ending + default (per GOVERNANCE.md's "Line Endings" section). There is no pytest suite, and `unittest` is + the runner instead. A script that carries a gate still earns tests, written with the standard + library's `unittest` so they run under bare `python3` with nothing installed, as + `test_<script>.py` under a `tests/` directory beside the scripts it exercises + (`<scripts-dir>/tests/`), kept apart so a test never reads as a tool. Within the scripts + directory the name carries the kind: a gate that checks and exits non-zero on a finding takes a + `_lint` or `_gate` suffix, and a utility that does work takes none. Any repo carrying Python + carries the Python tooling in CI, coverage included, this profile too: `uvx ruff@latest check`, + `uvx ruff@latest format --check`, `uvx mypy@latest`, and the unittest suite under + `uvx coverage@latest run -m unittest discover -s <scripts-dir>/tests` with `coverage report`, + informational with no threshold adopted. A co-present `csharp` type still carries `codecov.yml` + for its own tests. + +## Versioning + +**Published packages.** `_version.py` ships with `__version__ = "0.0.0"` as a placeholder. Until +you wire `_version.py` to something that increments (the usual options are `hatch-vcs`, a +version.json bridge, or manual bumps), no new PyPI versions will land, and publishing with +`skip-existing: true` keeps a stuck placeholder version from failing the run. + +**Source-only repos** (no PyPI publish, with a source-release on dispatch or no release at all) do +not need `_version.py`: keep a static `version` in `pyproject.toml` `[project]`, or let the +release pipeline's version source (e.g. NBGV plus `version.json`) own the tag. There is no publish +step to guard, so `skip-existing` does not apply. diff --git a/.github/skills/python-codestyle/references/testing.md b/.github/skills/python-codestyle/references/testing.md new file mode 100644 index 00000000..0dae3fd4 --- /dev/null +++ b/.github/skills/python-codestyle/references/testing.md @@ -0,0 +1,17 @@ +# Python Testing Conventions + +This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` and does not +use pytest, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) +are in `references/profiles.md`. + +Use `pytest` with configuration in `[tool.pytest.ini_options]`. Default invocation: +`uv run pytest`. + +- One test file per module under test, named `test_<module>.py`. +- Test functions named `test_<scenario>_<expected_behavior>`, descriptive and not numbered. +- Use fixtures (defined in `conftest.py` for shared ones, or per-test for narrowly-scoped) instead + of setup/teardown methods. +- **Avoid mocking when fakes work.** Hand-rolled fakes that implement the protocol you depend on + are usually clearer and break less than `unittest.mock` magic. +- **Test edge cases that the docstring promises**, not implementation details. If the test breaks + when you refactor without changing behavior, the test is asserting on an implementation detail. diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md new file mode 100644 index 00000000..9e675f0a --- /dev/null +++ b/.github/skills/repo-worktree/SKILL.md @@ -0,0 +1,229 @@ +--- +name: repo-worktree +description: >- + Mandates and mechanizes task isolation in ptr727/ProjectTemplate fleet repos: every task, + including a continuation of a prior session's task, creates its own git worktree on its own + feature branch before its first file edit, based on the branch work starts on (develop on both + fleet workflow models unless the task is explicitly about main-only content, never whichever + branch a tool defaulted to), preferring a registered worktree in the fleet layout and using a + standalone-clone fallback only when the executor cannot write to either the standard worktree + location or its Git metadata, and no available approval route grants access. Also wraps the + mechanics: + creating a worktree with git worktree add, the fleet layout convention, listing what is in + flight, preparing Husky.Net or Python pre-commit hooks in the new tree, and removing a + worktree and its branch after merge. Use this whenever about to create or edit files in a + fleet repo, whenever starting or resuming a task, whenever the task's branch is already + checked out in a shared checkout, and whenever creating, listing, or removing a worktree. + Triggers even when the session was launched in the primary checkout or the change looks like + a one-line fix, because the primary checkout is the maintainer's own surface and the incident + this guards against was two sessions sharing one checkout, each session's blanket add + committing the other's uncommitted files. +--- + +# Repo Worktree + +## Why This Exists + +Two agent sessions once ran concurrently in the same primary checkout, on the same feature +branch, neither knowing the other was in the tree. One session's commits swept in the other +session's uncommitted files, so two commits landed carrying work their subjects never mention, +committed by a task that never saw it. No rule fired at the moment it was violated, which is the +first file edit: the commit-time and review-time skills all run after a sweep has already +happened. This skill is that missing task-start surface. `GOVERNANCE.md` "Repository Boundaries +and Write Safety" keeps the isolation law and wins on any disagreement, and the mechanics below +are this skill's own content. + +## The Mandate + +- **Every task isolates into its own worktree before its first file edit.** All new work begins + by creating a unique worktree (or clone) on its own feature branch. The primary checkout is + the maintainer's own surface, so a session launched there isolates before writing rather than + after noticing contention. +- **A continuation re-isolates.** A session resuming prior work finds its branch already checked + out somewhere and naturally resumes there, and that instinct is the hazard: a branch sitting + checked out in a shared tree is exactly how two sessions end up in one checkout. Create a + fresh worktree for the continuation and check the branch out there. +- **The moment is the first file edit, not the commit.** By commit time another task's + uncommitted work can already be swept into the staging area, so isolating late protects + nothing. Reading anywhere is fine, and the worktree exists before the first write. +- **Someone else's tree stays theirs.** A branch that changes when nothing you did changed it, + or an edit of yours reverted with no conflict, means another task is live in that tree, and + the response is to stop rather than to re-apply the edit, per `GOVERNANCE.md` "Repository + Boundaries and Write Safety". + +## The Base Branch + +Base the worktree on the branch work starts on for the repository's model, not on whichever +branch a tool defaulted to. GitHub's own "default branch" setting reads `main`, but on both +fleet workflow models work starts on `develop`, so a worktree defaulted to "the default branch" +lands on `main` and silently misses everything merged to `develop` but not yet promoted. Branch +from `develop` unless the task is explicitly about `main`-only content, per `GOVERNANCE.md` +"Branching Model". Fetch immediately before creating and base on the remote ref, because a clone +is whatever it last fetched rather than the branch it names. + +The base clone is a fetch source, not a place to do task work. `fetch` and `worktree add` run +against it for that purpose, and outside "Listing and Cleanup"'s own terminal step below, nothing +else does: never `checkout`, `pull`, `reset`, `commit`, or any other command that mutates its own +working tree, index, or HEAD while a task is in progress. That distinction is the one a real +incident missed, reusing a primary checkout as the working directory itself rather than only as +the source a worktree is created from. On Claude Code this is also a mechanical stop for most +of that list. `merge --ff-only`/`pull --ff-only` and a `checkout <ref>`/`switch <ref>` carrying no +force flag stay exempt even there, matching this skill's own cleanup step below, which needs +exactly those. +Prose remains the only enforcement for a non-Claude-Code agent, and for the shapes the hook itself +exempts. + +## Creating a Worktree + +The fleet layout convention keeps every base clone and every in-flight task visible in one +place: + +```text +~/repos/<Repo> base clone, on its default/working branch +~/repos/worktrees/<Repo>-<task-slug> one worktree per in-flight task, own branch +~/repos/upstream/<owner>-<repo> clone of a repo under another owner, not a fork +``` + +The top level carries no owner segment because everything in it is the fleet owner's own, an +original repo and a fork alike. A fork is named `<upstream-owner>-<upstream-repo>` at fork time, +so a fork of `acme/core` is `acme-core`, and its name identifies the upstream project and stays +unique in the flat namespace without an owner segment of its own. A repository adopted as the +owner's own work rather than kept as a fork is detached from its parent and keeps a plain name, +`widget` rather than `initech-widget`, since it no longer tracks anything upstream. + +A clone of a repository under another owner is neither of those, and flattening one collides +rather than merely reading oddly: `acme/core` joined the way a fork is joined **is** the fork's +name, `acme-core`, while reduced to a bare `core` it names no project and collides with the next +`core` cloned from any other owner. Those clones live one level down under `upstream/`, named by +that same join, so `upstream/acme-core` sits beside the fork it would otherwise land on. The +segment states the relationship rather than the owner, so a reference checkout is told from a +working repo without a `git remote` call, and the names under it never compete with the flat +namespace above. The join is ambiguous in the abstract, since a hyphen in either half means +`acme-labs/core` and `acme/labs-core` produce one name, and it is kept anyway because it is the +fork convention's own join: the ambiguity is inherited from the flat namespace above rather than +introduced here, and it surfaces at clone time as a directory that already exists, where the +second clone takes a hand-picked name. A worktree off one of them keeps the flat worktrees path +under the same name, `~/repos/worktrees/<owner>-<repo>-<task-slug>`. Contributing a change from +such a clone is never a push out of it: fork the upstream first, per the +`upstream-contribution-workflow` skill, and that fork's own clone then belongs in the flat +namespace above, under the name this one already has. + +```sh +git -C ~/repos/<Repo> fetch origin develop +git -C ~/repos/<Repo> worktree add ~/repos/worktrees/<Repo>-<task-slug> -b <task-branch> origin/develop +``` + +The registered worktree above is the normal path. It keeps the task visible in `git worktree +list`, the fleet worktree directory, and IDE worktree discovery. It also leaves the task in a +durable location the maintainer can inspect after the agent session ends. + +Before running that command, inspect the executor's active write boundaries. A linked worktree +requires write access to both of these locations: + +- the intended `~/repos/worktrees/<Repo>-<task-slug>` worktree directory +- the base clone's `.git/worktrees/` administrative directory, which holds the index and locks + +A writable worktree directory does not make the administrative directory writable. When the +executor has an approval mechanism, request scoped approval for the standard `git worktree add` +command. A declared sandbox boundary is the reason to request that approval, not by itself the +reason to skip the registered worktree. + +Use the standard layout when both locations are writable or the executor approves the scoped +write. When approval is unavailable or denied, create a standalone clone under a writable +temporary root. Name it `<temporary-root>/<Repo>-<task-slug>`, fetch immediately, and create the +task branch from `origin/develop`. A standalone clone keeps its worktree and Git administrative +directory under the same writable root. It therefore supports edits, explicit-path staging, +commits, and branch updates without sharing the base clone's index. + +On Claude Code, a standalone clone is structurally a primary checkout to `gh-write-guard.py`'s own +rule 6 test (`--git-dir` equals `--git-common-dir` there too, since it is not a linked worktree of +anything), so the hook denies the very commits/edits this fallback exists to make. Set +`GH_WRITE_GUARD_ALLOW_PRIMARY_CHECKOUT` for the session before using one -- the same escape hatch +`host-setup/agent-safety/README.md`'s requirement 6 already documents -- since this fallback is +exactly the narrow, already-approval-gated case that grant exists for. + +A temporary standalone clone is a degraded handoff, not an equivalent location. The base clone +does not register it, `git worktree list` does not show it, and an IDE opened on the base clone +does not discover its changes. The maintainer must navigate to it manually, and the operating +system may reap it as temporary data. State the absolute path as soon as the fallback is chosen +and again in the handoff. Do not present work there as ordinarily reviewable from the primary +workspace. + +```sh +TASK_ORIGIN="$(git -C <base-clone> remote get-url origin)" +git clone --no-checkout "$TASK_ORIGIN" <temporary-root>/<Repo>-<task-slug> +git -C <temporary-root>/<Repo>-<task-slug> fetch origin develop +git -C <temporary-root>/<Repo>-<task-slug> switch -c <task-branch> origin/develop +``` + +Do not use a linked worktree under the temporary root when the base clone's Git metadata is +read-only. If an existing linked worktree must be kept, index operations require the executor's +scoped approval for that administrative path. Use the standalone clone only after the standard +registered path and its approval route are unavailable. + +A continuation attaches the task's existing branch rather than forking a fresh one: + +```sh +git -C ~/repos/<Repo> fetch origin <task-branch> +git -C ~/repos/<Repo> worktree add ~/repos/worktrees/<Repo>-<task-slug> <task-branch> +``` + +When the base clone holds only the remote-tracking ref, the same command creates the local +branch tracking `origin/<task-branch>` through git's ordinary checkout guessing, so a fresh +clone needs no separate branch setup. Git refuses to attach a branch that is already checked +out somewhere else, and that refusal is the mandate working, since the branch sitting checked +out in a shared tree is the hazard the continuation rule exists for. Return that checkout to +its own working branch first when its tree is clean, and stop when it is not, because a dirty +tree there may be another task's uncommitted work. + +A machine not yet migrated to this layout still isolates exactly the same way, since the mandate +is the isolation rather than the path: create the worktree beside whatever layout the machine +has, and note that the base clone may live elsewhere than `~/repos/<Repo>`. + +## Agent-Specific Worktree Tools + +Provider-specific mechanics stay separate from the general creation procedure above: + +- **Claude Code:** its `EnterWorktree` tool acts only on an explicit instruction from the user or + project instructions. Given a `name`, it creates the worktree under `.claude/worktrees/` and + bases it on the GitHub default branch. Both differ from the fleet path and base. Create the + worktree with `git worktree add`, then attach with `EnterWorktree` `path:`, not `name:`. +- **Codex:** no provider-specific creation override applies. Use the general `git worktree add` + procedure above. Its host-specific writable-root setting lives in `docs/host-setup.md` "Agent + Worktree Access". +- **opencode:** no provider-specific creation override applies. Use the general + `git worktree add` procedure above. + +## Preparing Git Hooks + +A new worktree holds tracked hook configuration but not every generated hook runtime. Prepare +the hooks immediately after creating or attaching the worktree, before the first commit. A +shared `core.hooksPath` value does not make generated files such as `.husky/_/husky.sh` appear +in the new tree. + +- **Husky.Net:** When `.husky/pre-commit` sources `.husky/_/husky.sh` and the local .NET tool + manifest declares Husky.Net, run `dotnet tool restore`, then `dotnet husky install` from the + worktree root. +- **Python pre-commit:** When `.pre-commit-config.yaml` exists, run `uv tool install pre-commit` + once per host if not already installed, then `pre-commit install` from the worktree root. + `pre-commit` is never a project dependency, so this is the same regardless of profile. +- **Repository override:** Follow a repository's explicit hook-setup instructions when they + differ from these standard cases. Do not infer a replacement command from the language alone. + +Treat hook preparation as worktree setup, not as recovery after a rejected commit. If setup +fails, report that boundary and fix the setup. Never bypass the hook to make the commit succeed. + +## Listing and Cleanup + +- `git worktree list`, run in any checkout of a repo, names that repo's base clone and every + worktree with its branch. On the convention layout, one `ls ~/repos/worktrees/` reads what is + in flight across the whole fleet. +- **Cleanup after merge is the default terminal step.** Run it after a squash merge into `develop`. Run it again after a merge-commit promotion into `main`, unless the user explicitly says to retain a checkout or branch. A merge or release handoff is incomplete while finished task, conflict-resolution, installer, or release worktrees remain registered. +- **Verify before removing.** Read the pull request's merged state and head SHA from live GitHub state. Confirm the worktree is clean and resolves to that head. A dirty worktree stops cleanup because force-removing it would discard work. A detached helper worktree needs no pull request, but its commit must be contained in the branch whose completed operation created it. +- **Remove the exact finished worktree, then its local task branch.** Use `git worktree remove <exact-path>`. Try `git branch -d <exact-branch>` after a merge commit. A squash merge does not make the feature tip an ancestor of `develop`, so `-d` cannot recognize it as merged. After the live merged-PR and clean-worktree checks prove that exact branch finished, use `git branch -D <exact-branch>` under the narrow post-squash exception in `git-commit-conventions`. Never apply that exception to an unverified branch or to `develop`. +- **Remove temporary standalone clones and detached helper worktrees too.** Remove the exact `<temporary-root>/<Repo>-<task-slug>` path after confirming it is clean. The remote feature branch follows the repository's normal pull request cleanup policy. Never delete `develop` after a promotion because it is the permanent integration branch. +- **Return the base clone to current `develop`.** Fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching or reconciling it. +- **Prove the cleanup.** Finish with `git status --short --branch` in the base clone and `git worktree list`. The expected result is a clean base clone at `origin/develop` and no worktree belonging only to the completed task. +- A worktree that refuses removal is dirty, and force is not the fix: look at what is + uncommitted in it first, since discarding uncommitted work runs only on explicit instruction, + per the `git-commit-conventions` skill. diff --git a/.github/skills/resync-a-repo/SKILL.md b/.github/skills/resync-a-repo/SKILL.md new file mode 100644 index 00000000..67918990 --- /dev/null +++ b/.github/skills/resync-a-repo/SKILL.md @@ -0,0 +1,85 @@ +--- +name: resync-a-repo +description: >- + Drives RESYNC.md's procedure for bringing a ptr727/ProjectTemplate fleet repo that is already + stood up back into line with the current hub, run from a hub checkout against a named target + repo. Use this whenever asked to resync, sync, converge, or bring a specific repo up to date + with the hub, or to run a conformance sweep against a named repo and apply what it finds. Needs + a hub checkout and a named target repo to mean anything, so it does not usefully trigger from + inside a downstream repo's own session with no target named and no hub checkout present, that + case is fleet-conformance-check instead. Triggers even when the request sounds routine, such as + "just copy AGENTS.md over" or "make repo X match the hub," because that phrasing is exactly how + the AGENTS.md-overwrite incident happened. +--- + +# Resync a Repo + +## Why this exists + +RESYNC.md's own apply order already sequences the remedies so the rules land before the files +they govern and a deletion lands before the re-vendor that would otherwise refresh it. The +AGENTS.md-overwrite incident happened inside that same procedure, on the step that looked most +routine. This skill exists so the mandatory check survives contact with a real, time-pressured +resync instead of depending on an agent remembering to run it unprompted. It is a driver over +RESYNC.md, not a replacement for it. Read RESYNC.md itself for the deletion sweep, the +letters-versus-drift routing, the settings and ruleset step, and everything else that does not +change from one resync to the next. + +## Confirm the procedure before starting + +Read RESYNC.md section 0. A repo with no instruction set at all, or a partial one, is not this +skill's job, it is STANDUP.md sections 1A and 2 instead, since an absent carried file is a +baseline that never arrived rather than drift to converge. Run `spec/audit.py <RepoName>`, the +target's `registry/repos.json` `name` field rather than an `owner/repo` slug or a checkout path, +and read whether the findings are letters (absent) or drift (present but stale) before doing +anything else. The finding kind names the procedure the repo is owed. + +## Reach the hub and measure before changing anything + +Fetch a hub checkout of your own immediately before reading it, per RESYNC.md section 1, since a +stale clone answers confidently instead of failing. Never operate against an existing checkout +already present at a known or shared path, the maintainer's own primary checkout included, even +one that looks current -- always fetch into a private worktree of your own, per `repo-worktree`. +On Claude Code this is now also a mechanical stop for most such commands (a `PreToolUse` hook +denies a mutating git operation run directly in a primary checkout), though this prose is still +the only enforcement for a non-Claude-Code agent, and for the narrow shapes the hook itself +exempts, so following it here is not optional even where no hook can catch a lapse. Verify the +host with `python3 scripts/host_gate.py --repo <path-to-target-checkout>`. Then run the audit end +to end, +RESYNC.md section 2, against the target's `main` branch, never `develop`. A finding is a snapshot, +so quote the run stamp in anything derived from it and re-run before acting on a finding read +earlier in the session. +File any hub defect this work exposes against `ptr727/ProjectTemplate`. +Examples include bugs, conflicting sources, unclear or incomplete instructions, missing capabilities, and Copilot findings about any of them. +Search open and closed issues first, then update the matching issue or file a new one. +Preserve the evidence RESYNC.md section 2 requires, and do not leave the finding only in chat, a review thread, the downstream repo, or agent memory. + +## Apply, in this order + +1. **The instruction set first.** `CLAUDE.md`, then `AGENTS.md` and `GOVERNANCE.md` verbatim sections, then `CODESTYLE.md` and `WORKFLOW.md`, including the `AGENTS.md` skill-dependency pointer paragraph (naming `scripts/skills_install.py` and where the fleet's Skills live) as one more verbatim unit carried in this same step, not a separate pass. `CLAUDE.md` is the single `@AGENTS.md`-import file that gets `AGENTS.md` into a Claude Code session's context at all, a separate baseline entry from `AGENTS.md` itself, so carrying one without the other still leaves that provider unconfigured. **Before touching `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, or `WORKFLOW.md` in this step, run the `carried-instruction-file-guard` skill's distinctive-phrase probe against the target file, every time, without exception, regardless of whether the update is a verbatim re-vendor or an intent-fidelity edit.** This is not advisory language to weigh against how routine the diff looks, a diff that looks routine is exactly the shape the AGENTS.md-overwrite incident took. Do not proceed to the re-vendor until the probe has run and any local addition it finds has a destination, per that skill's own procedure. `CLAUDE.md` is outside that guard's scope: it carries no mixed or repo-specific content by design, so its re-vendor is an ordinary verbatim-fidelity copy, no probe needed. +2. **Deletions second, before any re-vendor.** Only a `retire` disposition in + `spec/divergences.json` authorizes removing a file, and the removal is swept tree-wide, per + RESYNC.md section 4, before the deletion counts as done. +3. **Verbatim re-vendors** for `CLAUDE.md` and everything else the probe in step 1 cleared. A + finding classified modified rather than stale gets its diff read before being overwritten, + since it may be an improvement the hub should adopt instead of a mistake to erase. +4. **Interface workflows.** Honor the named contract, required jobs, the ruleset-bound check name, + the artifact-name handoff, rather than copying bytes. +5. **Settings, rulesets, and secrets.** Run + `repo-config/configure.sh check "<owner>/<repo>" release` (substitute `operational` for an + operational repo) from the hub at `main`, then `apply` for what it reports, never from a + carried copy. Run `spec/audit.py [RepoName]` from the same checkout for secrets. +6. **Intent files last, and by hand,** since nothing mechanical judges these. + +Reconcile the registry entry (`status`, `types`, `releaseTrigger`, `workflowModel`, +`driftNotes`) in the same pass, and delete a `driftNote` describing work this pass just finished +rather than leaving it standing. + +## Ship it + +One focused pull request per drift class, branched from the target's `develop`, never a direct +push to a protected branch and never a hand edit outside a pull request. Close the review loop, +per the `pr-review-conduct` skill, before asking the maintainer for merge permission. The +maintainer merges, the agent drives to green and stops. Re-run the audit after the merge and +commit the report once authorized, per `git-commit-conventions`, done means measured, not +applied. diff --git a/.github/skills/shell-codestyle/SKILL.md b/.github/skills/shell-codestyle/SKILL.md new file mode 100644 index 00000000..eaa4a583 --- /dev/null +++ b/.github/skills/shell-codestyle/SKILL.md @@ -0,0 +1,62 @@ +--- +name: shell-codestyle +description: >- + Governs Bash/shell script style for ptr727/ProjectTemplate fleet repos: when a bootstrap or + host-tool script may be shell instead of Python, the mandatory set -Eeuo pipefail header, the + pipefail-versus-early-reader pitfall, self-locating scripts, the shellcheck-plus-shfmt + clean-compile, and the why-not-what comment rule. Use this whenever writing, reviewing, or + editing a shell script (a `.sh` file, or an extensionless bash/sh shebang script), whenever + deciding whether a new script should be Bash or Python, or whenever a pipeline built from + `curl`/`grep`/`jq`-style commands looks like it silently swallowed a failure. Triggers even when + the task looks like a one-line tweak to an existing script, because a missing `-e`/`pipefail`, + or a reader piped straight from a producer that closes the pipe early, are each invisible until + the exact failure mode they guard against actually happens. Fleet-wide: a shell script can + appear in any repo (a bootstrap that installs the interpreter, a host tool that must run before + a toolchain exists), not only a repo whose primary language is shell. +--- + +# Shell Codestyle + +## Why this exists + +This is the shell-specific half of the fleet's code style guide, kept in one place instead of +re-derived per repo or per session. Shell is the fleet's exception language, reached for only +where Python cannot run yet, so its rules exist to keep that narrow surface safe rather than to +cover general scripting style. + +## When shell, not Python + +Bash, and only where a program cannot be Python: a bootstrap that installs the interpreter cannot +be written in it, and a host tool that must run before a development toolchain exists cannot +depend on one. Everything else is Python, with a test under the scripts tree's `tests/` directory. + +## Rules + +- **`shellcheck` is the linter and `shfmt` the formatter.** The clean-compile is `shellcheck` + clean at default severity plus `shfmt -d`, both reporting nothing before a commit. CI enforces + both, per `GOVERNANCE.md` "Running the Linters Locally", and `scripts/docker_lint.py` runs the + same pair headless. Neither is scoped to the `*.sh` glob alone: a tracked, extension-less + script whose shebang names bash or sh (the shape a script meant to run as a bare command takes) + joins the target list too. +- **`set -Eeuo pipefail`, before the first command the script runs.** A header comment sits above + it, as `repo-config/configure.sh` and the `host-setup/` scripts do, since what matters is that + nothing executes unguarded rather than which line number it lands on. Without `-e` a failed + command in the middle of a sequence lets the rest run against a state nobody checked, and + without `pipefail` a pipeline reports the exit of its last stage, so a fetch that failed reads + as an answer when a parser downstream succeeds on an empty input. `-E` carries an `ERR` trap + into functions and command substitutions, so a script that later adds one is not surprised by + where it does not fire. +- **A reader that stops early needs its producer read first.** Under `pipefail`, a producer + writing to a closed pipe exits non-zero, so `curl ... | grep -q` reports a successful fetch as + a failure whenever the match is found early enough. Capture the output, then search it. +- **Self-locating, never dependent on the caller's directory.** A script resolves its own + directory from `BASH_SOURCE` and references its payloads through it, since the working + directory at invocation is not a property of the script. +- **`shellcheck` clean, and a deliberate exception carries its reason inline.** A + `# shellcheck disable=SCxxxx` names why the rule does not apply here, so the next reader can + tell a considered exception from an unread warning. `repo-config/configure.sh` is the worked + example, carrying five `SC2016` disables where a single-quoted `jq` program must stay + unexpanded, each with its reason on the same line. +- **Comments say why, never what.** The code states what it does. A comment restating it goes + stale silently, where a comment carrying a reason fails visibly when the reason stops being + true. diff --git a/.github/skills/skill-lifecycle/SKILL.md b/.github/skills/skill-lifecycle/SKILL.md new file mode 100644 index 00000000..aa853916 --- /dev/null +++ b/.github/skills/skill-lifecycle/SKILL.md @@ -0,0 +1,49 @@ +--- +name: skill-lifecycle +description: >- + Governs the lifecycle of the fleet's own skills in ptr727/ProjectTemplate: creating, changing, splitting, and retiring a skill under .agents/skills/, the source-versus-generated split with .github/skills/ and .claude-plugin/, the regenerate and --check semantics of scripts/build_dist.py, the install and stamp semantics of scripts/skills_install.py, the doc-packaging pattern that keeps a law doc and its skill in agreement, and the trigger-description conventions that make a skill fire. Use this whenever about to create, edit, move, or delete anything under .agents/skills/, .github/skills/, or .claude-plugin/, whenever packaging a doc or a doc section as a skill, and whenever deciding whether a topic deserves a skill at all. Triggers even when the edit looks trivial, such as fixing a typo in one SKILL.md, because the generated distributions desync the moment the source changes without a build_dist.py run, and CI fails the pull request on exactly that. Hub-context only, since .agents/skills/ exists only in the hub. +--- + +# Skill Lifecycle + +## Why This Exists + +The agent most likely to get a skill wrong is the one editing a skill, and before this skill existed nothing watched that moment: the regenerate and install semantics lived in `scripts/` docstrings and scattered prose, so the procedure was rediscovered per session. The two standing hazards are mechanical and silent. A hand-edit to the generated `.claude-plugin/` tree is overwritten by the next regenerate, and a source edit without a regenerate ships a plugin that no longer matches its source, which the CI `--check` gate fails rather than anyone noticing in review. + +## The Pipeline + +- **`.agents/skills/<name>/SKILL.md` is the only hand-authored source**, with optional `references/` and `scripts/` directories beside it. Codex and opencode read this tree directly, project-local, and also read the global `~/.agents/skills/` copy the installer materializes. +- **Generated distributions serve GitHub Copilot and Claude Code.** `scripts/build_dist.py` generates `.github/skills/` for GitHub Copilot and a Claude-plugin-compatible copy at `.claude-plugin/fleet-skills/`, published through `.claude-plugin/marketplace.json`. Neither generated tree is hand-edited, and `build_dist.py --check` exits non-zero when either tree differs from `.agents/skills/`. +- **The skill set is implicit.** Every `.agents/skills/<name>/` directory carrying a `SKILL.md` is a skill, and the generated `plugin.json` derives its list from those directories, so adding or retiring a skill edits no manifest by hand. `marketplace.json` names the plugin, not the skills, and is untouched by ordinary lifecycle work. +- **`scripts/skills_install.py`, run from a hub checkout, installs both forms per machine**: an overlay copy into `~/.agents/skills/` for Codex and opencode, marked per skill so a retired skill is removed on the next run and a foreign skill is never touched, and a user-scope plugin install for Claude Code via the `claude` CLI. Each run stamps the hub commit into `~/.agents/skills-install-stamp.json`, and `--report` reads that stamp against the checkout and exits non-zero when the machine is behind. The install is global per user, and per-repo pinning is a settled non-goal (`docs/fleet-map.md` "Skills Install Model"). + +## Deciding a Topic Deserves a Skill + +A skill surfaces at a trigger moment. A rule that binds every action all the time, or a short reference section a task reads once, gains nothing from being one: the always-on layer is the carried instruction set (`AGENTS.md` and the sections it maps), and packaging it as a skill duplicates it and spends the tokens the delegation rules exist to save. The `AGENTS.md` "Where the Rules Live" map records the disposition either way, a skill annotation on the row or the deliberate absence of one, so a topic with no skill reads as a decision rather than an oversight. + +## Creating a Skill + +1. **Name the directory in kebab-case** and set the frontmatter `name:` to the same string. +2. **Write the `description:` to carry the trigger**, since it is the only part an agent reads before deciding to load the skill: state what the skill governs, then the concrete moments it applies ("Use this whenever..."), then the routine phrasings that precede the failure it guards against ("Triggers even when..."), naming a real incident where one exists. Disambiguate against sibling skills by name, the way `standup-a-repo`, `resync-a-repo`, and `fleet-conformance-check` each state which of the three a session is in. +3. **Author the body per the `comment-and-doc-style` skill**: LF (the repo default), present tense, ASCII tiers, no semicolon in prose. Name hub paths as plain code spans rather than repo-relative links, because an installed copy resolves no repo path, and say "from a hub checkout" for anything the reader must run. +4. **Split bulk into `references/`** when the source doc is large: the SKILL.md carries the summary and the binding rules, and each `references/*.md` carries one topic read on demand, the shape `comment-and-doc-style` uses. +5. **Apply the doc-packaging pattern below in the same change** when the skill packages a law doc or one of its sections. +6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then, once authorized, commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. +7. **Record the surfacing**: annotate the `AGENTS.md` "Where the Rules Live" row when the skill packages a GOVERNANCE section, or its closing paragraph when the skill is new content, so the map stays the one place coverage is read from. +8. **Refresh the machines after merge**: re-run `python3 scripts/skills_install.py` per machine, the cadence `docs/host-setup.md` "Fleet Skills Install" states. Until then every machine serves the previous skill set, which `--report` says. + +## Changing or Retiring a Skill + +- **Edit only the source tree.** Any skill-content change under `.github/skills/` or `.claude-plugin/` that did not come from a `build_dist.py` run is a defect, whatever it fixes. +- **Retiring is deleting the source directory and regenerating.** The derived `plugin.json` list shrinks with it, and the installer's per-skill markers remove the retired skill from `~/.agents/skills/` on each machine's next run. +- **A deletion sweeps the prose that references the skill**, in the same change rather than as follow-up: the `AGENTS.md` map row or paragraph naming it, any law-doc packaging pointer to it, and any sibling skill that disambiguates against it. A law-doc section that had moved its full rules into the skill takes them back, or is retired with it, so no rule is silently lost with the skill that carried it. +- **Renaming is a retire plus a create** as far as the installer's markers and the plugin list are concerned, so sweep references the same way. + +## The Doc-Packaging Pattern + +Packaging keeps one topic in one authoritative place while the skill makes it surface automatically. It has two shapes, and each pairing states which it uses: + +- **Moved content.** The law-doc section keeps a summary and the skill holds the full rules (`git-commit-conventions`, `comment-and-doc-style`, `pr-review-conduct`). The section ends with the standard pointer sentence: packaged as the named skill at `.agents/skills/<name>/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo, read the skill for the full rules. +- **Kept authority.** The source doc keeps the full rules and the skill is the summary that routes to them (`audit-a-repo` over `AUDIT.md`, `workflow-ci-contract` over `WORKFLOW.md`, `agent-conduct` over its GOVERNANCE sections). The skill states per topic which doc section owns it. + +In both shapes the doc wins on any disagreement, and the skill is what needs fixing. A rule stated fully in both places is the drift this pattern exists to prevent, so an edit to a packaged rule lands in its owning place and the other side's summary is checked against it in the same change. diff --git a/.github/skills/standup-a-repo/SKILL.md b/.github/skills/standup-a-repo/SKILL.md new file mode 100644 index 00000000..363d6bb3 --- /dev/null +++ b/.github/skills/standup-a-repo/SKILL.md @@ -0,0 +1,105 @@ +--- +name: standup-a-repo +description: >- + Drives STANDUP.md's procedure for taking a ptr727/ProjectTemplate fleet repo from nothing (or a + partial state) to operational against the fleet ground truth, run from a hub checkout for a + named target repo the maintainer is standing up. Use this whenever asked to stand up, create, + bootstrap, or onboard a new fleet repo, or to onboard a new repo type. Needs a hub checkout and + a target repo, new or partially started, to mean anything, so it does not usefully trigger + inside an already-operational downstream repo's own session with no hub checkout present, that + case is resync-a-repo for drift or fleet-conformance-check for a self-check instead. Triggers + even when the request sounds like "just copy the template over" or "spin up a quick repo," + because skipping the ordered signing, branch, and instruction-set steps below is exactly how a + repo ends up unsigned, unrecoverable, or authored against unknown rules. +--- + +# Stand Up a Repo + +## Why this exists + +STANDUP.md's own section order exists because several of its steps close a window that cannot be +reopened cheaply: commit signing has to be correct before the first commit, the long-lived +branches have to exist before any standup commit lands on one, and the instruction set has to be +carried before anything else is authored against it. This skill exists so that order survives +contact with a real, time-pressured standup instead of depending on an agent remembering to run +each gate unprompted. It is a driver over STANDUP.md, not a replacement for it. Read STANDUP.md +itself for the full text of every step, the onboarding-a-new-repo-type procedure, and the +cold-start self-test. + +## Before starting + +Read STANDUP.md section 0A first. Nothing in this procedure creates the GitHub repository, its +App, or its secrets, each an outward-facing write that needs the maintainer's explicit permission +and inputs, so hand that checklist over before step 1 rather than discovering the gap partway +through. A repo with no remote is not partially stood up, it is not started, and only the +maintainer can supply what section 0A lists. + +## Apply, in order + +1. **Signing, before the first commit.** STANDUP.md section 0: verify, never set, the inherited + `--global` commit identity and signing configuration, and the host tool floors via + `python3 scripts/host_gate.py`. The window closes at the first commit, since a repo committed + under the wrong identity or unsigned cannot be cleanly repaired afterward. + +2. **Branches, before the first standup commit.** STANDUP.md section 0B: create `main` and + `develop` empty, off one signed empty root commit, then run every step below on a feature + branch off `develop`. Never commit standup work directly onto `develop`. `non_fast_forward` on + both branch payloads, or the missing blocking rule on an operational repo's `develop` ruleset, + makes that mistake either unrecoverable or silently unprotected. + +3. **Classify and catalog.** STANDUP.md section 1: resolve the repo's type(s) against `AUDIT.md` + section 2, then write or repair its `registry/repos.json` entry and confirm it with + `spec/validate.py`. + +4. **The instruction set, before authoring anything.** STANDUP.md section 1A: carry `CLAUDE.md`, + `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md` and `AUDIT.md`, adapted rather + than cloned for the ones that describe a repo, plus `.markdownlint-cli2.jsonc` and + `cspell.json`. `CLAUDE.md` is the fixed, verbatim `@AGENTS.md`-import file that gets + `AGENTS.md` into a Claude Code session's context at all, a separate baseline entry from + `AGENTS.md` itself, so carrying one without the other still leaves that provider unconfigured. + Read `CODESTYLE.md` and the `GOVERNANCE.md` documentation-style rules before writing any repo + content of your own, the same window-closes shape as signing in step 1. + +5. **Capture the source, if one exists.** STANDUP.md section 1B, only when the repo's content + replaces a live external system: capture it and verify the capture against the source before + anything is scaffolded from it, since the source is not under version control and cannot be + re-derived once it stops serving. + +6. **The baseline files.** STANDUP.md section 2: copy every `spec/files.json` entry whose + `appliesTo` matches the repo's selector set, adapted rather than cloned, and choose + `version.json`'s version floor deliberately rather than propagating the template's. Carry + `AGENTS.md`'s skill-dependency pointer paragraph, naming `scripts/skills_install.py` and where + the fleet's Skills live, as one more verbatim unit in this same step, not a separate pass, the + identical requirement `RESYNC.md` places on a repo already stood up. + +7. **The workflows.** STANDUP.md section 3: implement the Actions `WORKFLOW.md` requires for the + repo's type, reusing `catalog/snippets/workflows/` as the reference implementation rather than + inventing a shape. + +8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub + repository agree before running anything else here, then run + `repo-config/configure.sh check owner/repo release` (substitute `operational` for an + operational repo) from the hub at `main`. A non-zero exit there means drift was found, not a + command failure. Review what it reports. Then run the same command's `apply` subcommand, which + idempotently reconciles the repo to the full committed configuration regardless of what `check` + reported, never from a hand-built or carried copy. + +9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood + up only when it passes for its type, or its residual deltas are tracked in + `reports/<repo>/audit.md` plus an issue. + +## Onboarding a new repo type + +When a repo matches no existing type in `spec/project-types.json`, that is a type to onboard, not +a repo to force into the nearest existing one. STANDUP.md's "Onboarding a New Repo Type" section +covers the manifest additions (`spec/project-types.json`, `spec/files.json`, `spec/secrets.json`, +`spec/scope-model.md`, `spec/type-model.md`, and the `registry/repos.schema.json` target enum for +a new publish destination) and the cold-start self-test that proves the result usable by a +context-free agent, not just by the one that wrote it. + +## Ship it + +One pull request per standup, branched from `develop` per step 2 above, into `develop`, never a +direct push to a protected branch. Close the review loop, per the `pr-review-conduct` skill, +before asking the maintainer for merge permission. The maintainer merges, the agent drives to +green and stops. diff --git a/.github/skills/upstream-contribution-workflow/SKILL.md b/.github/skills/upstream-contribution-workflow/SKILL.md new file mode 100644 index 00000000..eec4fcff --- /dev/null +++ b/.github/skills/upstream-contribution-workflow/SKILL.md @@ -0,0 +1,84 @@ +--- +name: upstream-contribution-workflow +description: >- + Governs how the maintainer contributes to a third-party repository he does not control (for + example esphome/esphome), distinct from the fleet's own internal branching model: a dirty work + branch on his own fork for the actual work and review iteration, squashed once clean to a second + branch that carries only the intended minimal history, that clean branch opened as the PR + against the upstream repo, and reviewer feedback applied to the dirty branch first, then + re-squashed into the clean one. Use this whenever about to open a pull request against a + repository outside the ptr727 fleet, whenever forking a third-party project to contribute a fix + or feature, whenever an upstream reviewer requests changes on a PR opened this way, and whenever + deciding which issue or PR template to use for a third-party repository. Triggers regardless of + the target repo's own type or workflow model, since this skill is about the shape of a + contribution to someone else's repo, not the target repo's own internal conventions, which this + skill does not attempt to state and are never assumed to match the fleet's. +--- + +# Upstream Contribution Workflow + +## Why this exists + +The fleet's own branching model (`operational-vs-release-workflow`) governs repos the maintainer +controls end to end: squash-only feature branches, merge-commit promotions, signed commits under +his own identity. None of that applies to someone else's repository. A PR into a third-party +project answers to that project's own maintainers, on their own timeline, with their own review +cycles, and the history that lands there should read as a deliberate, minimal contribution, not as +the maintainer's own iteration log. This skill is that different shape, kept separate from the +fleet's internal model so the two are never conflated. + +## The two-branch shape + +1. **Fork the upstream repo**, if not already forked. +2. **Do the actual work on a dirty work branch**, on the maintainer's own fork. This branch is + allowed to be messy: false starts, fixup commits, back-and-forth in response to review, whatever + the real work looks like while it's happening. Open a PR from this branch into a branch on the + maintainer's **own fork** (not upstream), so all the iteration happens there, visible and + reviewable, without touching the upstream repo at all. +3. **Once the dirty branch is clean and the change is ready, squash it to a second branch** that + carries only the intended, minimal commit history, one commit (or a small, deliberate set) that + states what the change is, not how it was arrived at. +4. **Open the PR against the upstream repo from that second, clean branch.** This is the only + branch upstream ever sees. An upstream draft may be opened only after that clean presentation + branch exists and is published. When more preparation is needed, continue on the dirty branch, + re-squash it into the clean branch, and update the same draft by the step 5 procedure. Never + iterate directly on the published presentation branch. Mark the draft ready when preparation + finishes. Open it ready immediately when no preparation remains. +5. **If upstream reviewers ask for changes, apply them to the dirty branch first**, iterate there + the same way as step 2, then re-squash the updated dirty branch into the clean branch that + actually reaches upstream. Updating the same upstream PR rather than opening a new one each + round rewrites the clean branch's history, and pushing a rewritten branch that is already + published requires `git push --force-with-lease` (prefer it over a bare `--force`, it refuses + the push if the remote moved since the last fetch). **`git-commit-conventions`'s never-force-push + rule governs this fleet's own repos, where a branch is shared with bots, other branches, and + required-check history a rewrite would orphan, and it stays absolute there, with no exception. + It has no jurisdiction here**: this clean presentation branch lives on the maintainer's own + fork, outside the fleet entirely, and carries nobody's work but this squash. Force-with-lease + is scoped just as tightly regardless: only this one branch, only on the maintainer's own fork, + never the dirty work branch, which is the append-only iteration log this whole workflow exists + to preserve. If force-with-lease is ever refused or unavailable, open a fresh PR from a newly + named clean branch rather than fighting the push. + +**The dirty branch is always the working copy. The clean branch is always the presentation copy.** +Never reverse this: never iterate directly on the branch that's open against upstream, and never +skip the squash step because the dirty branch "looks clean enough." + +## Use the upstream repo's own conventions, not the fleet's + +Always use the upstream repo's own issue and PR templates, its own contribution guidelines, and +its own commit-message and code-style conventions when they differ from this fleet's. The fleet's +`comment-and-doc-style`, `git-commit-conventions`, and `pr-review-conduct` skills describe how +*this fleet* does things, and none of them are the target repository's own rules. Read the target +repo's `CONTRIBUTING.md` (or equivalent) and follow it. Where the target repo states no +convention of its own, matching the surrounding code's existing style in that file is the better +default, not falling back to the fleet's own convention by habit. + +## What stays governed by the fleet's own rules + +Signing commits and using the correct git identity are host configuration, not project +convention, so `git-commit-conventions`'s signing and identity rules still apply on both the dirty +and clean branches. They are properties of the committer, not of the target repository. The +write-safety rules (never write to a repository outside explicit authorization, never fabricate a +GitHub id) also still apply in full. A fork the maintainer owns is within scope to push to, and +the upstream repository itself is written to only through the PR the maintainer explicitly asked +for. diff --git a/.github/skills/workflow-ci-contract/SKILL.md b/.github/skills/workflow-ci-contract/SKILL.md new file mode 100644 index 00000000..e02fdfb4 --- /dev/null +++ b/.github/skills/workflow-ci-contract/SKILL.md @@ -0,0 +1,47 @@ +--- +name: workflow-ci-contract +description: >- + Governs the WORKFLOW.md CI/CD behavioral contract for every ptr727/ProjectTemplate fleet repo: the D1-D9 guarantees stated as the failure mode each prevents, the seam contract for release assets, the artifact lifecycle, NBGV versioning and classification, validate-at-entry, and the 5A/5B/5C test methodology with its per-type walkthroughs. Use this whenever writing or editing anything under .github/workflows/, adding or dropping a release target, auditing a repo's workflows, or reasoning about why a publish did or did not fire. This is the YAML half of the pipeline, and the operational-vs-release-workflow skill keeps the git half (branching, promotion, publish policy), so branch choice questions go there. Triggers even when the edit looks mechanical, such as bumping an action, renaming a job, or adding one upload step, because SHA pinning, the ruleset-bound aggregator name, smoke gating on uploads, and retention-days are each easy to break in a one-line diff that no smoke build exercises, since workflow-only changes are deliberately not smoke-built. WORKFLOW.md keeps authority, and GOVERNANCE.md wins where the two overlap. +--- + +# Workflow CI Contract + +## Why This Exists + +`WORKFLOW.md` in the hub is the largest law doc, a behavioral contract stating required outcomes rather than a required implementation, and it had no skill surface, so agents edited workflow YAML without the contract in view. This skill is the summary plus the binding rules, with the guarantee catalog and the test methodology split into `references/`. `WORKFLOW.md` keeps authority for the contract and methodology, and `GOVERNANCE.md` ("Workflow YAML Conventions", "Release Model") wins where the two overlap. + +## How the Contract Is Read + +- **Outcomes, not bytes.** A workflow is correct when it satisfies the section 4 contract against the expected inputs and outputs, not when it matches a catalog snippet byte for byte. Two repos may implement one guarantee with different YAML. +- **Applicability.** A guarantee governing a construct the repo does not contain is N/A: recorded, excluded from the verdict, never a defect. A source-only pipeline is mostly N/A and that is fine. +- **Operational is binary.** Every applicable guarantee holds, or the workflow is not operational. A single applicable input-output mismatch is a defect regardless of how clean the YAML looks. +- **Reached, not carried.** A standard workflow whose job graph is identical across repos of a type is a `workflow_call` task the hub hosts once, and a repo carries only a caller stub pinned to a hub release commit plus a composite-action hook at `.github/actions/<hook>` for what is its own. A hub task reaches its own actions and sibling tasks through `$/`, which resolves at that pinned commit. The merge-bot is the first, and `docs/reusable-workflows.md` in the hub carries the model, the hook contract, and the phase each workflow migrates in. Until a workflow's phase ships, its copy is graded as below. +- **Two layers.** Orchestration (the PR entry workflow, publisher, version/release/badge jobs) is generic and standard at the job level. Build leaves (`build-<target>-task.yml`) are repo-owned. Inputs like `github`/`nuget`/`dockerhub`/`expect_release_assets` live on the orchestrator, a leaf only receives `ref`/`branch`/`smoke` and a derived `push`, so assert each input in the layer that declares it. What a repo curates is the list of targets, and adding or dropping one edits the whole surface together: the `enable_<target>` input, the `build-<target>` job and its `github-release` `needs:` entry, the `changes` paths-filter entry and output, and the `smoke-build` enable-forward (D6.4). + +## Style Rules That Break in One-Line Diffs + +- **Pin every action to a commit SHA** with a trailing `# vX.Y.Z` comment, first-party included. The one documented no-pin exception is `dotnet/nbgv@master`. Invent no others. +- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and the hub's `repo-config/` payloads in lockstep, or required-check enforcement silently breaks. +- **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'` with `cancel-in-progress: true`. The publisher is the documented exception: a global ref-independent group with `cancel-in-progress: false`, so publishes serialize and never cancel mid-push. +- **Shells**: every multi-line bash `run:` starts `set -Eeuo pipefail`. Multi-line `if:` uses `>-`, never `|`. +- **Boolean inputs** are declared in both trigger blocks and compared against both forms, `${{ inputs.foo == true || inputs.foo == 'true' }}`, since `workflow_dispatch` delivers strings. +- **Permissions validate before `if:`**, so even a skipped job needs valid `permissions:`, and a callee's extra scope (`actions: write`, `id-token: write`) is granted by the caller at the one entry point that needs it. +- **Chaining across optional jobs** allowlists `success`/`skipped` explicitly, because `!= 'failure'` lets `cancelled` through. +- **Docker layer cache** targets a registry tag (`buildcache-<branch>`), never `type=gha`. +- **Workflow YAML is LF.** Preserve endings on every edit. + +## The Core Behavioral Spine + +- **PRs validate fast and never publish**: a paths-filter smoke-builds only changed targets, a type-appropriate validation job always runs, and one required aggregator gates the merge, treating skipped smoke as pass and blocking on failure or cancelled. Smoke does a full compile/lint/test but pushes nothing and uploads nothing, every `upload-artifact` gated `!smoke`. +- **A human merge never auto-publishes**: a `plan` job decides once and every job gates on it. Publishes come from a code-affecting bot push to `main`, a manual dispatch of `main` or `develop`, or the main-only weekly Docker schedule. Each run builds the one trigger branch, `main` a clean `X.Y.Z`, anything else a prerelease `X.Y.Z-g<sha>`, with NBGV owning the patch from git height. The release tags the built commit's SHA (`GitCommitId`), never a branch name. +- **Validate at entry**: cross-input and input-versus-derived-state invariants are asserted once in a dedicated entry job the downstream jobs `needs:`, failing fast with `::error::` before expensive work. The release gate checks branch-versus-prerelease in both directions, strips `+buildmetadata`, and on smoke skips the check while the job still succeeds. +- **The seam contract**: a target contributes a release file by uploading `release-asset-<branch>-<target>`, and the release job collects by `pattern:` plus `merge-multiple:`, never `artifact-ids:`, canonical even for a single target. A repo with no file target passes `expect_release_assets: false` at the caller. +- **Artifacts are an intra-run handoff**: consume-then-delete at the point of consumption, gated to the consumer's condition, best-effort, `retention-days: 1` on every upload as the backstop, and never a blanket delete of the run's artifact set, which destroys the diagnostics you need when the run fails. +- **No-op republish**: an unchanged version re-pushes nothing, the release-create step skips when the tag exists, registries dedupe server-side (`--skip-duplicate`, `skip-existing: true`), and Docker alone always re-pushes by design. +- **A build failure blocks every publish target**: `github-release` needs every build, and the terminal registry pusher guards with `!failure() && !cancelled()`, so nothing partial ships. + +The full catalog, each guarantee with the failure mode it prevents, is in `references/d-guarantees.md`. Auditing, tracing, and probing a repo's workflows is `references/test-methodology.md`. + +## After Any Workflow Edit + +Workflow-only changes are not smoke-built, so run actionlint locally (the Docker invocation in `GOVERNANCE.md` "Running the Linters Locally", which bundles shellcheck for `run:` blocks) before pushing, and remember a workflow change is only fully exercised by CI, since `secrets: inherit`, `permissions:`, and `needs:` wiring resolve only in a real run. diff --git a/.github/skills/workflow-ci-contract/references/d-guarantees.md b/.github/skills/workflow-ci-contract/references/d-guarantees.md new file mode 100644 index 00000000..b41c99a6 --- /dev/null +++ b/.github/skills/workflow-ci-contract/references/d-guarantees.md @@ -0,0 +1,70 @@ +# The D-Guarantees, Condensed + +Each guarantee is a MUST from `WORKFLOW.md` section 4, stated as input to output plus the failure mode it prevents. This is the condensed catalog for working from, and `WORKFLOW.md` keeps authority, so read the section there when a guarantee's exact wording decides a verdict. + +## D1: PR Fast-Feedback (Smoke) + +- **D1.1** Only changed targets build: each target has a paths-filter entry, unchanged targets skip. Prevents a changed target slipping through unbuilt. +- **D1.2** A validation job always runs on any PR, and a non-.NET repo replaces it (never deletes it), re-pointing every `needs:` on it, the aggregator and `smoke-build` both. Prevents a PR merging with no validation, or a dangling `needs:` failing the workflow to load. +- **D1.3** Smoke never publishes and never uploads: full compile/lint/test, no pushes, every `upload-artifact` gated `!smoke`. Prevents a PR publishing and orphaned artifacts. +- **D1.4** Workflow-file changes are not smoke-built (the filter excludes `.github/workflows/**`), actionlint still validates them. +- **D1.5** One required aggregator gates merge: `needs:` the changes and validation jobs, passes on skipped smoke, blocks on failure or cancelled, and its name is ruleset-bound (job `name:` equals ruleset `context:`, renamed together). +- **D1.6** Coverage reports to Codecov for C# and Python repos with tests, best-effort so an outage never reds the gate, with a `codecov.yml` setting statuses informational and `.gitignore` excluding coverage output. + +## D2: Validation at Entry + +- **D2.1** A dedicated entry job asserts each cross-input invariant before expensive work, downstream jobs `needs:` it. +- **D2.2** The release gate fails loud when the default branch carries a prerelease suffix or a non-default branch carries none, strips `+buildmetadata` first, and on smoke skips the check while the job still succeeds (a job-level `if:` would skip dependents with it). +- **D2.3** A dispatch publish from any ref other than `main` or `develop` fails fast. +- **D2.4** Mutually-exclusive or must-pair inputs are validated, a half-filled combination fails fast. + +## D3: Versioning and Classification + +- **D3.1** One branch per run: `github.ref` names the built branch, NBGV classifies it directly, no `IGNORE_GITHUB_REF`. +- **D3.2** Default branch yields `X.Y.Z`, every other branch `X.Y.Z-g<sha>`, and the default-branch literal in the gate, the `prerelease` expression, and `version.json`'s `publicReleaseRefSpec` all name the repo's real default branch. +- **D3.3** `version.json` sets the major.minor floor, NBGV appends git height as the patch, and both are retained even by a no-compiler repo, since they own the tag. +- **D3.4** Registry versions follow the classification per registry: NuGet.org derives prerelease from the SemVer2 suffix, PyPI builds from `AssemblyFileVersion` with `.dev0` appended on `develop` only, and the develop build stays `--pre`-selectable above the released version. +- **D3.5** A wrapper repo drives its image version from a committed `name -> version` state file, and the leaf must actually read it, since a leaf still tagging off NBGV means the wrapper is not pinned to upstream. + +## D4: Release and Publish + +- **D4.1** Gated single-branch publish: a human merge never auto-publishes, the `plan` job decides once, publishes come from a code-affecting bot push to `main`, a dispatch of `main`/`develop`, or the main-only weekly Docker schedule. +- **D4.2** `target_commitish` is the built commit's SHA (NBGV `GitCommitId`), never a branch name and never `github.sha`. +- **D4.3** Every release is a tag plus source zip, README, and LICENSE, file targets attach `release-asset-*`, and a no-file-target caller passes `expect_release_assets: false` or the release-create step fails on unmatched files. +- **D4.4** No-op republish: an unchanged version re-pushes nothing, the release-create skips when the tag exists (refreshed only on `workflow_dispatch`), registries dedupe server-side, and Docker always re-pushes by design. +- **D4.5** A failed build blocks every publish target: `github-release` needs every build, the terminal registry pusher guards `!failure() && !cancelled()`, so nothing partial ships. +- **D4.6** A deploy check asserts which release and which environment answer, waiting for convergence to a bounded timeout, with an unreachable host reported distinctly from an HTTP status. + +## D5: Resource Cleanup + +- **D5.1** A cross-job transfer artifact is deleted at its point of consumption. An in-run intermediate may rely on the retention backstop. +- **D5.2** The delete runs under the same condition as its consumer, so a no-op re-run skips the release-asset delete while the PyPI build-artifact delete still runs. +- **D5.3** Cleanup is best-effort (`continue-on-error`, tolerate a failed listing, delete all matching ids). +- **D5.4** Every `upload-artifact` sets `retention-days: 1`. +- **D5.5** Never blanket-delete the run's artifacts, which destroys diagnostics and auto-emitted build records. +- **D5.6** A durable deploy destination's retention is bounded by a declared count with one side recorded as owning the prune: the deploy where its credential can observe the destination, the host where the credential is deliberately write-only. + +## D6: Seam Conformance + +- **D6.1** The release job downloads by `pattern:`/`merge-multiple:`, never `artifact-ids:`, canonical for single-target repos too. +- **D6.2** Branch-derived config reads `inputs.branch`, never `github.ref_name`. +- **D6.3** Artifact names are branch-suffixed. +- **D6.4** A target add or drop updates the whole surface together: `enable_<target>` input, `build-<target>` job, `github-release` `needs:` entry, paths-filter entry and output, and the `smoke-build` enable-forward. + +## D7: Concurrency, Permissions, Safety + +- **D7.1** The publisher serializes: global ref-independent concurrency group, `cancel-in-progress: false`. +- **D7.2** Every reusable job declares valid `permissions:` (validated before `if:`), a callee's extra scope granted by the caller. +- **D7.3** Boolean inputs are declared in both trigger blocks and compared against both forms. +- **D7.4** Optional-dependency chaining allowlists `success`/`skipped` explicitly. + +## D8: Bots and Automation + +- **D8.1** The merge-bot enables auto-merge on `opened`/`reopened` for every Dependabot tier, dispatches squash or merge by base ref, disables on a maintainer-pushed `synchronize`, and keys concurrency on the PR number, not `github.ref`. +- **D8.2** Codegen runs a deterministic matrix over both branches, Dependabot targets both branches. +- **D8.3** The upstream tracker writes a committed `name -> version` state file via a rolling per-branch bump PR the merge-bot auto-merges, and its branch prefix must match the merge-bot's head-ref pairs or auto-merge silently never fires. +- **D8.4** An identity allowlist used as a gate emits a `::warning::` on the non-matching branch rather than falling through silently, since a renamed App slug otherwise turns the gate off invisibly. + +## D9: Style and Static + +SHA pins with version comments, the name-suffix rules, `set -Eeuo pipefail`, `if: >-`, registry-tag Docker cache with `cache-to` only the built branch on push and `cache-from` both branches, line endings per `.editorconfig`. diff --git a/.github/skills/workflow-ci-contract/references/test-methodology.md b/.github/skills/workflow-ci-contract/references/test-methodology.md new file mode 100644 index 00000000..ecd09487 --- /dev/null +++ b/.github/skills/workflow-ci-contract/references/test-methodology.md @@ -0,0 +1,27 @@ +# Testing a Repo's Workflows + +The three escalating verification modes from `WORKFLOW.md` section 5, which keeps authority. N/A items (a check or scenario for an absent construct) are recorded and excluded, never failed. + +## 5A: Static Audit + +Read the workflow files plus `version.json` and assert the structural fact behind each applicable D-guarantee, each pass, fail, or N/A with a `file:line` citation, asserting each input in the layer that declares it. The core sweep covers: the paths-filter's target coverage and `.github/workflows/**` exclusion, smoke gating on every upload, the aggregator's `needs:` and skip/fail handling, the entry validation jobs and the two-directional release gate, the single-branch NBGV classification and the three default-branch literals agreeing, `target_commitish` from `GitCommitId`, the consume-then-delete artifact lifecycle with `retention-days: 1` everywhere and no blanket delete, the `pattern:` handoff and `inputs.branch` config, the publisher's serialized concurrency, and the SHA pins. `WORKFLOW.md` 5A lists the per-type addenda (console runtime matrix, NuGet `--skip-duplicate`, the PyPI OIDC environment split, Docker `expect_release_assets` and cache shape, the static-site deploy gates), so apply only the ones the repo's types imply. + +## 5B: Trace Scenarios + +For each applicable scenario, evaluate every job's `if:`/`needs:` against the inputs and compare the predicted run/skip, version, release, and artifact end state to the expected table in `WORKFLOW.md` 5B. The load-bearing ones: + +- **S1** a PR touching a target: that target smoke-builds, nothing uploads, the aggregator succeeds. +- **S5/S6** a bot push to `main`: publishes only when code-affecting, and a human push never does. +- **S7** a publish run builds the one trigger branch with the right classification and leaves no dangling artifacts. +- **S8** a dispatch from a ref other than `main`/`develop` fails fast. +- **S9** a no-op re-run: release-create skipped, registries dedupe, PyPI build artifact still deleted, Docker still re-pushes. +- **S10** branch and version classification disagree: the gate fails loud and everything downstream skips. +- **S12/S13** a deploy dispatch: ref gate first, environment re-asserted, pointer flip separate, live check names the release, and a production deploy from a non-default ref fails before anything is written. + +## 5C: Live Probe + +Only for what a static trace cannot settle: a trivial PR to confirm S1, a smoke push-probe of both branches' version classification, registry queries after a real publish, and the artifact lifecycle read from a real run's logs. The deploy ref gate is verified only by tripping it, and that dispatch is the maintainer's to run: the agent prepares the command and reads back the four evidence items (gate conclusion, its error text, every downstream job skipped, deployment count unchanged), and a harness refusal to fire it is the control working, never something to re-shape. + +## Verdict + +Operational iff every applicable 5A item passes and every applicable 5B scenario matches, with the failing guarantees and their triggering inputs named, and the N/A list recorded. Per-project-type walkthroughs mapping scenarios onto targets, including source-only, static-site, and operational shapes, are `WORKFLOW.md` section 6. diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index c66d4dcf..18ae2baf 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -153,7 +153,7 @@ jobs: - name: Create tags and args step id: tagsargs run: | - set -euo pipefail + set -Eeuo pipefail TAGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Tags) }}') ARGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Args) }}') CACHE_REPO=$(jq -r '.[0]' <<< '${{ toJson(matrix.images.Tags) }}' | sed 's/:[^:]*$//') diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 8e9b47b5..d8cb6cc6 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -53,7 +53,7 @@ jobs: (steps.metadata.outputs.package-ecosystem != 'nuget') || (steps.metadata.outputs.update-type != 'version-update:semver-major') run: | - set -euo pipefail + set -Eeuo pipefail case "${{ github.event.pull_request.base.ref }}" in develop) method=--squash ;; main) method=--merge ;; @@ -97,7 +97,7 @@ jobs: - name: Merge pull request step run: | - set -euo pipefail + set -Eeuo pipefail case "${{ github.event.pull_request.base.ref }}" in develop) method=--squash ;; main) method=--merge ;; diff --git a/.github/workflows/publish-plan-task.yml b/.github/workflows/publish-plan-task.yml index fe493cb7..ee1c4fae 100644 --- a/.github/workflows/publish-plan-task.yml +++ b/.github/workflows/publish-plan-task.yml @@ -57,7 +57,7 @@ jobs: ACTOR: ${{ inputs.actor }} REF: ${{ inputs.ref_name }} run: | - set -euo pipefail + set -Eeuo pipefail publish=false case "$EVENT" in workflow_dispatch) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index fb918cda..d1333921 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -137,7 +137,7 @@ jobs: env: SEMVER2: ${{ needs.get-version.outputs.SemVer2 }} run: | - set -euo pipefail + set -Eeuo pipefail CORE_AND_PRE="${SEMVER2%%+*}" # drop +buildmetadata; a '-' here is the genuine prerelease separator if [[ "$CORE_AND_PRE" == *-* ]]; then echo "::error::Public (main) release version '$SEMVER2' carries a prerelease suffix; refusing to publish." @@ -152,7 +152,7 @@ jobs: GH_TOKEN: ${{ github.token }} TAG: ${{ needs.get-version.outputs.SemVer2 }} run: | - set -euo pipefail + set -Eeuo pipefail if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "exists=true" >> "$GITHUB_OUTPUT" if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then @@ -169,7 +169,7 @@ jobs: # Skip when the release already exists, but always let a manual `workflow_dispatch` through to refresh it. # Every release is a tag on the built commit plus the auto-attached source zip, README, and LICENSE. This is a # Docker-only repo: it ships no `release-asset-*` binaries/packages, so the release carries no extra files and - # `fail_on_unmatched_files` is omitted (see Template adaptations in AGENTS.md). The image is the published artifact + # `fail_on_unmatched_files` is omitted (see Template Adaptations in ARCHITECTURE.md). The image is the published artifact # on Docker Hub. - name: Create GitHub release step if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} @@ -205,7 +205,7 @@ jobs: - name: Resolve repository list step id: list run: | - set -euo pipefail + set -Eeuo pipefail # shellcheck disable=SC2016 # the jq program is a literal, not a shell expansion REPOS=$(jq --compact-output \ '[.Images[].Name | ascii_downcase | "ptr727/\(.)"] + ["ptr727/nx-base","ptr727/nx-base-lsio"] | sort | unique' \ diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index 64f59169..cac3628a 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -52,13 +52,13 @@ jobs: - name: Run codegen step run: | - set -euo pipefail + set -Eeuo pipefail dotnet run --project ./CreateMatrix/CreateMatrix.csproj -- \ matrix --versionpath=./Make/Version.json --matrixpath=./Make/Matrix.json --updateversion - name: Format code step run: | - set -euo pipefail + set -Eeuo pipefail dotnet tool restore dotnet husky install dotnet csharpier format --log-level=debug . diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 10b75001..9089c1e6 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -20,6 +20,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Deny by default: every job below grants only what its own steps need. +# A job added later inherits nothing rather than the repository default, which is write on this repo. +permissions: {} + jobs: # Inline change-gate (no third-party action): diff the push against its before-commit to decide whether the @@ -30,6 +34,8 @@ jobs: name: Detect image changes job if: ${{ !github.event.deleted }} runs-on: ubuntu-latest + permissions: + contents: read outputs: image: ${{ steps.filter.outputs.image }} base: ${{ steps.filter.outputs.base }} @@ -46,7 +52,7 @@ jobs: BEFORE: ${{ github.event.before }} AFTER: ${{ github.sha }} run: | - set -euo pipefail + set -Eeuo pipefail # A new branch or non-push event has no usable before-commit (all-zeros or empty). Build everything. if [[ "$BEFORE" =~ ^0+$ || -z "$BEFORE" || "${{ github.event_name }}" != "push" ]]; then echo "image=true" >> "$GITHUB_OUTPUT" @@ -74,6 +80,8 @@ jobs: validate: name: Validate job if: ${{ !github.event.deleted }} + permissions: + contents: read uses: ./.github/workflows/validate-task.yml secrets: inherit @@ -88,6 +96,8 @@ jobs: name: Smoke build job needs: [changes, validate] if: ${{ !github.event.deleted && needs.changes.outputs.image == 'true' }} + permissions: + contents: read uses: ./.github/workflows/build-docker-task.yml secrets: inherit with: @@ -99,18 +109,19 @@ jobs: branch: ${{ github.ref_name == 'main' && 'main' || 'develop' }} build_base: ${{ needs.changes.outputs.base == 'true' }} - # Single required status check. Its name is the ruleset-bound context in repo-config/ruleset-*.json. Do not - # rename it without updating those in lockstep. Skips on a branch deletion (when the other jobs also skip), so - # the deletion push does not fail this required check. + # Single required status check. Its name is the ruleset-bound context in the live branch ruleset. + # Do not rename it without updating the ruleset in lockstep. + # Skips on a branch deletion, when the other jobs also skip, so the deletion push does not fail it. check-workflow-status: name: Check pull request workflow status job runs-on: ubuntu-latest + permissions: {} needs: [changes, validate, smoke-build] if: ${{ always() && !github.event.deleted }} steps: - name: Check workflow results step run: | - set -euo pipefail + set -Eeuo pipefail exit_on_result() { # Pass on success or skipped (smoke-build is skipped when no image files changed). Fail only on # failure/cancelled. diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml index 6dd21b79..278baac9 100644 --- a/.github/workflows/validate-task.yml +++ b/.github/workflows/validate-task.yml @@ -21,6 +21,8 @@ jobs: validate: name: Validate job runs-on: ubuntu-latest + permissions: + contents: read steps: @@ -37,7 +39,7 @@ jobs: # Husky runs the same CSharpier + dotnet format style checks the editor and the pre-commit hook run. - name: Check code style step run: | - set -euo pipefail + set -Eeuo pipefail dotnet tool restore dotnet husky install dotnet husky run @@ -62,9 +64,18 @@ jobs: - name: Check EditorConfig step run: docker run --rm -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest - # --collect drives coverlet.collector to emit Cobertura XML into ./coverage/<guid>/. + # global.json opts into the native Microsoft.Testing.Platform runner, so the VSTest bridge + # (and its --collect data collector) is gone. + # --coverage-output stays unset because pinning one filename gives every test project in the solution the same path, and the last to finish overwrites the rest. + # The default <guid>.cobertura.xml written instead is a name codecov-cli's finder does not match, so each report is prefixed rather than renamed, keeping the guid that makes it unique. - name: Run unit tests step - run: dotnet test --collect:"XPlat Code Coverage" --results-directory ./coverage + run: | + set -Eeuo pipefail + dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage + for report in ./coverage/*.cobertura.xml; do + [ -e "$report" ] || continue + mv "$report" "./coverage/coverage-$(basename "$report")" + done # Report-only: fail_ci_if_error is false so a Codecov hiccup or an absent token never fails the gate. - name: Upload coverage to Codecov step diff --git a/.gitignore b/.gitignore index 193ff244..67a587d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,16 @@ -[Dd]ebug/ -[Rr]elease/ -[Bb]in/ -[Oo]bj/ - -.idea -.vs -.artifacts -.DS_Store -*.user +[Dd]ebug/ +[Rr]elease/ +[Bb]in/ +[Oo]bj/ + +.idea +.vs +.artifacts +.DS_Store +*.user + +# Coverage output (dotnet Microsoft.Testing.Extensions.CodeCoverage) +coverage/ +[Tt]est[Rr]esults/ +*.cobertura.xml +*.coverage diff --git a/.husky/task-runner.json b/.husky/task-runner.json index dbcd9602..a40c5ebe 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -1,32 +1,32 @@ -{ - "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", - "tasks": [ - { - "name": "CSharpier Format", - "command": "dotnet", - "args": [ - "csharpier", - "format", - "--log-level=debug", - "${staged}" - ], - "include": [ - "**/*.cs" - ] - }, - { - "name": ".NET Format", - "command": "dotnet", - "args": [ - "format", - "style", - "--verify-no-changes", - "--severity=info", - "--verbosity=detailed" - ], - "include": [ - "**/*.cs" - ] - } - ] -} +{ + "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", + "tasks": [ + { + "name": "CSharpier Format", + "command": "dotnet", + "args": [ + "csharpier", + "format", + "--log-level=debug", + "${staged}" + ], + "include": [ + "**/*.cs" + ] + }, + { + "name": ".NET Format", + "command": "dotnet", + "args": [ + "format", + "style", + "--verify-no-changes", + "--severity=info", + "--verbosity=detailed" + ], + "include": [ + "**/*.cs" + ] + } + ] +} diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 4afb100e..ed90f846 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -1,15 +1,18 @@ -{ - "config": { - // Prose paragraphs and data-heavy tables/URLs are intentionally long; - // reflowing at 80 cols hurts readability and churns diffs. - "MD013": false, - // Inline HTML is used for reference-link section dividers. - "MD033": false, - // Require fenced code blocks over the legacy 4-space-indented style. - "MD046": { "style": "fenced" }, - // MD060 (table column style) is not enforced - allow both compact - // (`|a|b|`) and padded (`| a | b |`) table pipe spacing. - "MD060": false - }, - "gitignore": true -} +{ + "config": { + // Prose paragraphs and data-heavy tables or URLs are intentionally long. + // Reflowing at 80 columns hurts readability and churns diffs. + "MD013": false, + // MD033 (inline HTML) stays enabled so native Markdown wins. + // HTML comments, used as reference-link dividers, pass it. + // The details and summary elements are allowed for GitHub collapsibles, which have no Markdown equivalent. + // Every other element still flags. + "MD033": { "allowed_elements": ["details", "summary"] }, + // Require fenced code blocks over the legacy 4-space-indented style. + "MD046": { "style": "fenced" }, + // MD060 (table column style) is not enforced - allow both compact + // (`|a|b|`) and padded (`| a | b |`) table pipe spacing. + "MD060": false + }, + "gitignore": true +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 660c933a..9e0e25b8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,43 +1,43 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Create Version", - "type": "coreclr", - "request": "launch", - "preLaunchTask": ".NET Build", - "program": "${workspaceFolder}/CreateMatrix/bin/Debug/net10.0/CreateMatrix.dll", - "args": ["version", "--versionpath=./Make/Version.json"], - "cwd": "${workspaceFolder}", - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": "Create Matrix", - "type": "coreclr", - "request": "launch", - "preLaunchTask": ".NET Build", - "program": "${workspaceFolder}/CreateMatrix/bin/Debug/net10.0/CreateMatrix.dll", - "args": ["matrix", "--versionpath=./Make/Version.json", "--matrixpath=./Make/Matrix.json", "--updateversion"], - "cwd": "${workspaceFolder}", - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": "Create Docker and Compose Files", - "type": "coreclr", - "request": "launch", - "preLaunchTask": ".NET Build", - "program": "${workspaceFolder}/CreateMatrix/bin/Debug/net10.0/CreateMatrix.dll", - "args": ["make", "--versionpath=./Make/Version.json", "--makedirectory=./Make", "--dockerdirectory=./Docker", "--versionlabel=Beta"], - "cwd": "${workspaceFolder}", - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - } - ] -} +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Create Version", + "type": "coreclr", + "request": "launch", + "preLaunchTask": ".NET Build", + "program": "${workspaceFolder}/CreateMatrix/bin/Debug/net10.0/CreateMatrix.dll", + "args": ["version", "--versionpath=./Make/Version.json"], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": "Create Matrix", + "type": "coreclr", + "request": "launch", + "preLaunchTask": ".NET Build", + "program": "${workspaceFolder}/CreateMatrix/bin/Debug/net10.0/CreateMatrix.dll", + "args": ["matrix", "--versionpath=./Make/Version.json", "--matrixpath=./Make/Matrix.json", "--updateversion"], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": "Create Docker and Compose Files", + "type": "coreclr", + "request": "launch", + "preLaunchTask": ".NET Build", + "program": "${workspaceFolder}/CreateMatrix/bin/Debug/net10.0/CreateMatrix.dll", + "args": ["make", "--versionpath=./Make/Version.json", "--makedirectory=./Make", "--dockerdirectory=./Docker", "--versionlabel=Beta"], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 65652b24..ac574da3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,175 +1,175 @@ -{ - "version": "2.0.0", - "tasks": [ - // .NET language group. A non-.NET repo drops this group and adds its own - // language's tasks. The first three tasks are the .NET clean-compile set - // (CODESTYLE.md) carried verbatim; the rest are convenience/project-specific. - { - "label": ".NET Build", - "type": "process", - "command": "dotnet", - "args": [ - "build", - "${workspaceFolder}", - "--verbosity=diagnostic" - ], - "group": "build", - "problemMatcher": [ - "$msCompile" - ], - "presentation": { - "showReuseMessage": false, - "clear": false - } - }, - { - "label": ".NET Format", - "type": "process", - "command": "dotnet", - "args": [ - "format", - "style", - "--verify-no-changes", - "--severity=info", - "--verbosity=detailed" - ], - "problemMatcher": [ - "$msCompile" - ], - "presentation": { - "showReuseMessage": false, - "clear": false - }, - "dependsOrder": "sequence", - "dependsOn": [ - "CSharpier Format", - ".NET Build" - ] - }, - { - "label": "CSharpier Format", - "type": "process", - "command": "dotnet", - "args": [ - "csharpier", - "format", - "--log-level=debug", - "." - ], - "problemMatcher": [ - "$msCompile" - ], - "presentation": { - "showReuseMessage": false, - "clear": false - } - }, - // Convenience / project-specific tasks (adapt or drop per repo). - { - "label": ".NET Tool Update", - "type": "process", - "command": "dotnet", - "args": [ - "tool", - "update", - "--all" - ], - "problemMatcher": [ - "$msCompile" - ], - "presentation": { - "showReuseMessage": false, - "clear": false - } - }, - { - "label": "Husky.Net Run", - "type": "process", - "command": "dotnet", - "args": [ - "husky", - "run" - ], - "problemMatcher": [ - "$msCompile" - ], - "presentation": { - "showReuseMessage": false, - "clear": false - } - }, - { - "label": ".NET Outdated Upgrade", - "type": "process", - "command": "dotnet", - "args": [ - "outdated", - "--upgrade:prompt" - ], - "problemMatcher": [ - "$msCompile" - ], - "presentation": { - "showReuseMessage": false, - "clear": false - } - }, - // Lint group - the local full doc-lint surface (Docker at :latest), matching the CI lint set. - // Run on demand. The pre-commit hook does language formatting only. Every repo carries these. - { - "label": "Lint: EditorConfig", - "type": "process", - "command": "docker", - "args": [ "run", "--rm", "--pull=always", "-v", "${workspaceFolder}:/check", "-w", "/check", "mstruebing/editorconfig-checker:latest" ], - "problemMatcher": [], - "presentation": { - "showReuseMessage": false, - "clear": false - } - }, - { - "label": "Lint: Workflows", - "type": "process", - "command": "docker", - "args": [ "run", "--rm", "--pull=always", "-v", "${workspaceFolder}:/repo", "-w", "/repo", "rhysd/actionlint:latest", "-color" ], - "problemMatcher": [], - "presentation": { - "showReuseMessage": false, - "clear": false - } - }, - { - "label": "Lint: Markdown", - "type": "process", - "command": "docker", - "args": [ "run", "--rm", "--pull=always", "-v", "${workspaceFolder}:/workdir", "-w", "/workdir", "davidanson/markdownlint-cli2:latest", "**/*.md" ], - "problemMatcher": [], - "presentation": { - "showReuseMessage": false, - "clear": false - } - }, - { - "label": "Lint: Spelling", - "type": "process", - "command": "docker", - "args": [ "run", "--rm", "--pull=always", "-v", "${workspaceFolder}:/workdir", "-w", "/workdir", "ghcr.io/streetsidesoftware/cspell:latest", "--no-progress", "README.md", "HISTORY.md" ], - "problemMatcher": [], - "presentation": { - "showReuseMessage": false, - "clear": false - } - }, - { - "label": "Lint: All", - "dependsOrder": "sequence", - "dependsOn": [ - "Lint: EditorConfig", - "Lint: Workflows", - "Lint: Markdown", - "Lint: Spelling" - ], - "problemMatcher": [] - } - ] -} +{ + "version": "2.0.0", + "tasks": [ + // .NET language group. A non-.NET repo drops this group and adds its own + // language's tasks. The first three tasks are the .NET clean-compile set + // (CODESTYLE.md) carried verbatim; the rest are convenience/project-specific. + { + "label": ".NET Build", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}", + "--verbosity=diagnostic" + ], + "group": "build", + "problemMatcher": [ + "$msCompile" + ], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + { + "label": ".NET Format", + "type": "process", + "command": "dotnet", + "args": [ + "format", + "style", + "--verify-no-changes", + "--severity=info", + "--verbosity=detailed" + ], + "problemMatcher": [ + "$msCompile" + ], + "presentation": { + "showReuseMessage": false, + "clear": false + }, + "dependsOrder": "sequence", + "dependsOn": [ + "CSharpier Format", + ".NET Build" + ] + }, + { + "label": "CSharpier Format", + "type": "process", + "command": "dotnet", + "args": [ + "csharpier", + "format", + "--log-level=debug", + "." + ], + "problemMatcher": [ + "$msCompile" + ], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + // Convenience / project-specific tasks (adapt or drop per repo). + { + "label": ".NET Tool Update", + "type": "process", + "command": "dotnet", + "args": [ + "tool", + "update", + "--all" + ], + "problemMatcher": [ + "$msCompile" + ], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "Husky.Net Run", + "type": "process", + "command": "dotnet", + "args": [ + "husky", + "run" + ], + "problemMatcher": [ + "$msCompile" + ], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + { + "label": ".NET Outdated Upgrade", + "type": "process", + "command": "dotnet", + "args": [ + "outdated", + "--upgrade:prompt" + ], + "problemMatcher": [ + "$msCompile" + ], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + // Lint group - the local full doc-lint surface (Docker at :latest), matching the CI lint set. + // Run on demand. The pre-commit hook does language formatting only. Every repo carries these. + { + "label": "Lint: EditorConfig", + "type": "process", + "command": "docker", + "args": [ "run", "--rm", "--pull=always", "-v", "${workspaceFolder}:/check", "-w", "/check", "mstruebing/editorconfig-checker:latest" ], + "problemMatcher": [], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "Lint: Workflows", + "type": "process", + "command": "docker", + "args": [ "run", "--rm", "--pull=always", "-v", "${workspaceFolder}:/repo", "-w", "/repo", "rhysd/actionlint:latest", "-color" ], + "problemMatcher": [], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "Lint: Markdown", + "type": "process", + "command": "docker", + "args": [ "run", "--rm", "--pull=always", "-v", "${workspaceFolder}:/workdir", "-w", "/workdir", "davidanson/markdownlint-cli2:latest", "**/*.md" ], + "problemMatcher": [], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "Lint: Spelling", + "type": "process", + "command": "docker", + "args": [ "run", "--rm", "--pull=always", "-v", "${workspaceFolder}:/workdir", "-w", "/workdir", "ghcr.io/streetsidesoftware/cspell:latest", "--no-progress", "README.md", "HISTORY.md" ], + "problemMatcher": [], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "Lint: All", + "dependsOrder": "sequence", + "dependsOn": [ + "Lint: EditorConfig", + "Lint: Workflows", + "Lint: Markdown", + "Lint: Spelling" + ], + "problemMatcher": [] + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 31a800ba..f4785acf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,237 +1,113 @@ -# Instructions for AI Coding Agents - -This repository builds and publishes Docker images for Network Optix VMS products (Nx Witness, Nx Meta, Nx Go, DW Spectrum, Wisenet WAVE). It includes base images (nx-base, nx-base-lsio) and derived product images that use the base images, plus a .NET tooling project (`CreateMatrix`) that generates Dockerfiles and build matrices and scripts/templates for packaging. There is **no NuGet publish**: the .NET project is a build-time matrix generator only; the published artifacts are exclusively the Docker Hub images. - -This file is the canonical reference for cross-cutting AI-agent rules. The CI/CD workflow contract and conventions live in [`WORKFLOW.md`](./WORKFLOW.md); the repository configuration-as-code (branch rulesets, settings, required secrets) lives in [`repo-config/`](./repo-config/); C# code-style conventions live in [`CODESTYLE.md`](./CODESTYLE.md). Copilot review *mechanics* are owned by [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) - this file delegates them there explicitly (see "PR Review Etiquette" below). - -**Where rules live.** A durable project, code, or style rule belongs in this file (or `WORKFLOW.md` / `CODESTYLE.md` as appropriate), so it is versioned and read by every session and every agent. An agent's own session memory or scratch state is private and lost on restart, so it is never the system of record for a rule: when you learn or are corrected on a rule, write it into the right doc in the same change. Memory may also note it, but the committed docs are the source of truth. - -For comprehensive coding and formatting standards, follow: - -- `CODESTYLE.md` -- `.editorconfig` -- `.husky/task-runner.json` -- `.vscode/tasks.json` - -## Solution Structure - -### Projects - -- `CreateMatrix/CreateMatrix.csproj` - - .NET 10 console app. - - Fetches product release metadata and generates build inputs such as Dockerfiles and matrix data used by CI. -- `CreateMatrixTests/CreateMatrixTests.csproj` - - xUnit v3 test project with AwesomeAssertions. - -### Key Directories - -- `Docker/` - - Generated and static Dockerfiles for base images and product variants. -- `Make/` - - Build orchestration scripts and test compose files. -- `Unraid/` - - Unraid container templates. -- `version.json`, `Make/Version.json`, `Make/Matrix.json` - - Version and matrix inputs consumed by the build pipeline. - -## Build and Validation Workflow - -- Primary developer entry points are the `CreateMatrix` CLI commands invoked directly or via the scripts in `Make/`: - - `version --versionpath=./Make/Version.json`. - - `matrix --versionpath=./Make/Version.json --matrixpath=./Make/Matrix.json --updateversion`. - - `make --versionpath=./Make/Version.json --makedirectory=./Make --dockerdirectory=./Docker --versionlabel=Beta`. -- Formatting and style checks are enforced by Husky.Net and VS Code tasks. -- Required tasks are documented in `CODESTYLE.md` and `.husky/task-runner.json`. -- C# code should be formatted with CSharpier, then verified with `dotnet format` (style). -- The `.NET Format` VS Code task in `.vscode/tasks.json` must be clean and warning-free at all times. - -### Workspace and linting - -- VS Code settings, extension recommendations, and spell-check words live in the workspace file `NxWitness.code-workspace`; add new editor settings or recommended extensions there rather than in `.vscode/`. (Build/debug tasks still live in `.vscode/tasks.json` and `.vscode/launch.json`.) Open the workspace file in VS Code (not the folder) so its settings and recommendations apply. -- Linting is editor-only (no CI lint job); the extensions recommended in `NxWitness.code-workspace` cover the project's structured files: C# (Roslyn + CSharpier), Markdown, Dockerfiles/Compose, GitHub Actions workflows, and spelling. Lint changed files and clear reported problems before pushing. -- For workflow files, the GitHub Actions extension covers schema/expression checks in-editor; run the `actionlint` CLI for deeper checks (including shellcheck on `run:` steps). - -## Image Architecture - -- Base images (`nx-base`, `nx-base-lsio`) are built and pushed, then reused as `FROM` images for derived product Dockerfiles. -- Derived product images should stay aligned with the base image changes and tags (for example, the Ubuntu distro tag). - -## CI Pipeline (GitHub Actions) - -The full CI/CD contract - triggers, jobs, the one-branch publish model, versioning, and the multi-image build layer - is specified in [`WORKFLOW.md`](./WORKFLOW.md), the canonical guide. The summary below is a pointer; do not duplicate those rules. - -- CI runs on **push to every branch** (`test-pull-request.yml`): it validates (`validate-task.yml`: Husky lint + `dotnet test`) on every push, and runs a fast smoke build (`build-docker-task.yml` with `smoke: true` - NxMeta + NxMeta-LSIO, amd64, no push) only when image files (`Docker/**`, `Make/Matrix.json`, `Make/Version.json`) change, via an inline `git diff` change-gate. One aggregator job, `Check pull request workflow status job`, is the ruleset-bound required check. -- Publishing is **triggered-Docker, one branch per run** (`publish-release.yml`): triggers are the weekly schedule (rebuilds `main` only), a path-scoped push to `main` on `Make/Matrix.json` (publishes a new codegen product pin at once), and manual dispatch (publishes the started-from branch). One run computes the version once (`get-version-task.yml`), builds the shared base once (main only; develop reuses it with `build_base: false`), builds the full product matrix from `Make/Matrix.json`, and on `main` cuts the GitHub release and pushes the Docker Hub overviews. -- Merges to `main`/`develop` do not build or publish images by themselves; only the matrix-pin push (main), a schedule, or a dispatch publishes. Auto-merged Dependabot and codegen PRs land commits the next publish picks up. Do not reintroduce a two-branch publish matrix, a nested `get-version` in the build task, the date-badge or standalone docker-readme workflows, or `dorny/paths-filter`. -- Lint workflow edits before pushing (see [Workspace and linting](#workspace-and-linting)); `validate-task.yml` runs the unit tests + Husky style checks in CI. - -## Versioning - -The `version` (major.minor) in [version.json](./version.json) is the NBGV version floor; NBGV appends the git height. **`develop` leads `main` by a minor:** after a `develop -> main` release lands and main's publish completes, bump the minor in `version.json` on `develop` in an isolated `bump-version-X.Y` PR (X.Y = the new minor), so develop's NBGV prerelease version stays numerically above main's last stable. A **maintenance** `develop -> main` promotion (dependency bumps, CI/doc fixes, template re-syncs) holds main's version - `git checkout main -- version.json` on the promotion branch - so `main` advances only its NBGV height, not its minor. (NBGV's version is the GitHub release tag on `main` and the `LABEL_VERSION` build arg baked into the images; the Docker image *tags* carry the Nx product version from `Make/Matrix.json` - see [CI Pipeline](#ci-pipeline-github-actions).) - -- A significant one-time overhaul of the build/release process (such as the branch-scoped CI/CD migration) is a deliberate maintainer-directed floor bump in the PR that introduces it, distinct from the routine cadence above; routine dependency, CI/workflow, and doc edits leave `version.json` untouched. -- **`dotnet/nbgv` is consumed via `@master`, never SHA-pinned.** Its tag stream lags `master` such that Dependabot tag-tracking would only propose downgrades to stale tags; this is the sole [`WORKFLOW.md`](./WORKFLOW.md) D9.1 action-pinning exception (rationale inline in `get-version-task.yml`). Do not SHA-pin it. - -## Git and Commit Rules - -- **Default to staging, not committing.** Stage changes with `git add` and leave `git commit` to the developer unless the developer has explicitly authorized the agent to commit for the current ask ("commit this", "open a PR", etc.). Authorization is scope-bound - it covers the commits needed for that specific task, not a blanket commit license for the rest of the session. -- **All commits must be cryptographically signed (SSH or GPG).** Branch protection enforces this on both branches; unsigned commits are rejected on push. Signing depends on environment configuration - `git config commit.gpgsign true`, a configured `user.signingkey`, and a working signing agent (loaded `ssh-agent` for SSH, or `gpg-agent` for GPG). If signing is not configured in the environment, **do not commit** - surface the missing config to the developer and stop at `git add`. Verify before any agent-authored commit (`git config --get commit.gpgsign && ssh-add -L` or the GPG equivalent). **Signing must be live before the *first* commit, not retrofitted.** Turning on `Require signed commits` against a branch that already has unsigned commits forces a rewrite of that entire history to re-sign it - changing every commit SHA and making whoever does the rewrite the committer and signer of every commit (a rebase preserves the `author` field but not the original signatures; you cannot sign another contributor's commits for them). During new-repo setup, never create commits until signing is verified. -- **Commit under the committing account's own GitHub `noreply` identity - never a private, personal, or invented address.** The `author` and `committer` on every agent-authored commit are the GitHub `noreply` address of the account whose key signs the commit (above) - GitHub issues these in a `username@users.noreply.github.com` or `ID+username@users.noreply.github.com` form, and for this single-maintainer fleet it is the owner's `ptr727@users.noreply.github.com`. Do not set `user.name`/`user.email` to a fabricated persona, bot name, or product name, and do not commit under whatever identity the environment happens to carry: verify `git config --get user.email` is that GitHub `noreply` address before committing, and fix it if not. A wrong identity is not cosmetic - a private email trips GitHub's email-privacy push protection (GH007), and an unrecognized or invented author pollutes history. Identity is separate from signing: a wrong author does not by itself fail the signature rule, but the ad-hoc identities that produce it are typically also unsigned, which the signing rule above then rejects on push. -- **Never force push.** Do not run `git push --force` or `git push --force-with-lease` under any circumstances. Force pushing rewrites shared history and can cause data loss. -- **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. -- **Mirror to `develop` any change that lands on `main` outside the feature -> develop -> main flow.** A reconciliation-branch fix made to resolve a `develop -> main` promotion conflict, or a security PR that merges only to `main`, leaves `develop` behind on that content - and forward-only `develop` never back-merges to catch up (the same parallel-target principle as the bots). Before basing new work on `develop`, or diagnosing a defect from it, check `git diff origin/develop origin/main`: a non-empty content diff means develop is stale and the defect may already be fixed on `main`. -- **Put issue-closing keywords (`Closes #N`) in the `develop -> main` promotion PR, not the feature or develop PR.** GitHub auto-closes an issue only from the PR (or commit) that merges to the default branch (`main`); a `Closes #N` that merges only to `develop` does not fire on promotion and leaves the issue open. Tag the promotion PR's description, or close the issue manually once the fix reaches `main`. - -## Pull Request Title and Commit Message Conventions - -### Format - -- Imperative subject summarizing the change, <=72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".) -- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*. - -### Rules - -- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.) -- Don't add `Co-Authored-By:` lines unless the developer explicitly asks. -- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. Nerdbank.GitVersioning computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected. -- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*). - -### Examples - -```text -Add structured logging extensions to library -Pin softprops/action-gh-release to commit SHA -Drop net8.0 multi-targeting from console project -Bump xunit.v3 from 3.2.2 to 3.3.0 -Clarify devcontainer setup steps in README -``` - -## Documentation Style Conventions - -### Markdown - -- Use reference-style links for any URL referenced more than once or appearing in lists; alphabetize the reference definitions block. -- Inline single-use relative links (e.g. `[CODESTYLE.md](./CODESTYLE.md)`) are fine. -- One logical paragraph per line; no hard-wrap line-length limit. For an intentional hard line break within a block - stacked badges, status, or license lines - end the line with a trailing backslash (`\`); this explicit form is preferred over trailing whitespace and is not treated as a paragraph split. -- Headings follow the title-case-with-short-bind-words rule from the PR-title section. -- **Write docs in the current state, not as a change from a prior one.** The reader has no memory of the previous behavior, so describe what *is*: "X does Y", never "X *now* does Y", "X *no longer* does Z", or "changed/switched/restored to Y". Before/after framing belongs in changelogs, commit messages, and PR descriptions - not in `README.md` or other living docs. - -### Comments - -Applies to code and workflow (`#`) comments alike. - -- Comment only when the code is non-obvious or important. Self-evident code needs no comment. -- Judge "obvious" in context, not line by line. A note that reads as redundant on its own line can be essential in the larger flow - a comment marking a workflow step's exit condition, for example, even though the line itself plainly does a `return` or `exit`. -- State the non-obvious *why*, not what the code already shows. No cross-project references (do not name other repos), no historic or design narrative, no rule citations - governance lives in this file, not echoed inline. -- **One line if it fits in ~120 columns.** Do not wrap a comment at 75-80 columns; a short two-line comment that would fit on one line looks sloppy - collapse it. Go multi-line only when the content genuinely exceeds ~120, filling each line rather than narrow-wrapping. For a multi-point comment, prefer short structured lines or `-` bullets over one prose paragraph. -- **Workflows: prefer one short summary description at the top of the file** over scattering rationale across steps; comment an individual step only when its purpose is non-obvious. -- **Do not accumulate comments.** When you change code or a comment, rewrite the whole comment fresh; never bolt a new comment onto an existing one or layer explanations across edits. Comment volume should stay flat or shrink over time, not grow. -- **Leave human-authored comments and emojis exactly as written** - do not reword, trim, reflow, or "clean" them, even if they seem to bend a rule. Revise only agent-authored comments, and match the surrounding voice when you do. - -### Character Set - -- **Write ASCII in all agent-authored text** - documentation, code, comments, commit messages, and PR descriptions. The agent does not introduce non-ASCII characters. Replace typographic Unicode with its ASCII equivalent on sight: - - em dash (U+2014) and en dash (U+2013) -> hyphen `-` (use a spaced ` - ` for an em-dash-style clause break) - - right arrow (U+2192) -> `->`; double arrow (U+21D2) -> `=>` - - less-than-or-equal (U+2264) -> `<=`; greater-than-or-equal (U+2265) -> `>=` - - curly quotes (U+2018/U+2019/U+201C/U+201D) -> straight `'` and `"`; ellipsis (U+2026) -> `...` -- **Allowed non-ASCII (two narrow exceptions):** - - **Scientific or technical symbols with no clean ASCII equivalent** - e.g. ohm, micro, degree, pi. Keep the symbol; do not approximate it away. - - **Unicode the developer deliberately typed** - emoji used for emphasis or as callout markers (for example the warning/info markers a maintainer placed in `README.md`). Preserve it; never strip the developer's own characters. This carve-out is for developer-authored text, not a license for the agent to add emoji. - -### Line Endings - -- [`.editorconfig`](./.editorconfig) defines the correct ending per file type (CRLF for `.md`, `.cs`, XML/`.csproj`/`.props`, `.yml`/`.yaml`, `.json`, `.slnx`, `.cmd`/`.bat`/`.ps1`; LF for `.sh` and for workflow YAML under `.github/workflows/*.{yml,yaml}` - Dependabot and Actions rewrite workflow files with LF, so they are pinned LF while all other YAML stays CRLF, and CI's `editorconfig-checker` enforces it), and [`.gitattributes`](./.gitattributes) pins `Dockerfile`/`*.Dockerfile` and `*.sh` to LF (`text eol=lf`) and otherwise stops git from normalizing. -- **Editing an existing file: preserve its current line endings** - do not reflow them as a side effect of a content change, even if the file is already non-compliant. After any programmatic edit, verify with `git diff --stat` (only changed lines) and `grep -c $'\r'` (CRLF count), since `file` does not report CRLF for JSON. Bring a non-compliant file to its `.editorconfig` ending only as a deliberate, isolated EOL-only change. - -### Quantitative Claims - -- Any quantitative claim in `README.md` (counts, sizes, version floors, supported products) must be verified against current code/config. If a doc number is derived from a code or matrix constant, mark the dependency in a source-code comment so the next editor knows to update both. - -## PR Review Etiquette - -> **Mandatory in every derived repo.** This entire "PR Review Etiquette" section is the provider-agnostic review-loop *contract* and must be carried **verbatim** into every repo derived from this template, alongside the [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) "GitHub Copilot Review Runbook" that implements it. Without both in-repo, an agent working in the derived repo has no pointer to the reliable Copilot mechanics and falls back to ad-hoc (and known-broken) behavior. - -The repo runs a review loop on every PR: local agent iteration plus remote automated review (GitHub Copilot is the configured reviewer). Treat this as a contract regardless of which local agent authored the changes. - -### Merge Gate (read this first) - -**Do not merge - and do not enable auto-merge - unless ALL of these hold:** - -1. Required status checks are green (`mergeStateStatus: CLEAN`), **and** -2. A Copilot review is confirmed on the **current head SHA** (not an earlier push), **and** -3. **Every** Copilot finding on that head SHA is closed out - all review threads resolved, **and** any issue-level Copilot comments (which have no resolve action) triaged and replied to - so zero outstanding findings remain, **and** -4. The maintainer has given **explicit** permission to merge. - -`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized). - -**Merging is not releasing.** A merge to a release branch does **not** by itself publish; publishing is a separate, explicitly configured step in the repo's release pipeline (e.g. a scheduled run, a manual dispatch, or an opted-in publish-on-merge trigger), not an automatic consequence of merging. Never describe a merge as cutting a release, and never trigger a publish without explicit maintainer instruction. - -### Expected Review Loop - -1. Push changes to the PR branch. -2. Re-request a review for the **current head SHA**. Auto-trigger is unreliable, so request it explicitly via the `requestReviews` GraphQL mutation (now reliable end-to-end - see the runbook); the UI is only a fallback. -3. Wait for review activity on that head. A completed review that raises **no findings** is a valid terminal outcome for that head - proceed; do not re-trigger it or treat the absence of comments as a missing review. -4. Triage findings. -5. Apply fixes or write a rationale for declines. -6. Reply to each thread and resolve what was addressed. -7. Re-run the loop after every fix push until no actionable findings remain. - -Drive the loop to green - review confirmed on the latest head SHA and every actionable finding closed - then stop and apply the **Merge Gate** above: all four preconditions must hold, and `mergeStateStatus: CLEAN` alone never satisfies it. - -For provider-specific mechanics (how to request review, query review state, post replies, resolve threads), see the **GitHub Copilot Review Runbook** in [.github/copilot-instructions.md](./.github/copilot-instructions.md). This file owns the contract; that file owns the mechanics. - -### Triaging Review Comments - -For each comment, classify before responding: - -- **Bug** - wrong behavior, missing test coverage, or a real divergence between code and docs. Fix it. Reply with the fixing commit SHA when done. -- **Style/convention** - the comment cites a rule from this file or a language-specific style guide. Two cases: - - The cited rule matches what the existing codebase already does -> fix the offending code. - - The cited rule contradicts what's in the tree, or industry norm -> **update the rule instead of the code**. The rule is wrong, not the code. Bouncing the same code across rounds is the symptom of a wrong rule. Heuristic: three rounds on the same style category means the rule needs adjusting and the user should authorize the rule change. -- **Architectural opinion** - the comment proposes a different design ("constrain this to disabled-by-default", "move it elsewhere", "add a runtime guardrail"). This is judgment, not a bug. Surface it to the user with a recommendation; don't apply unilaterally. - -### Responding and Resolution Expectations - -Reply inline with either the fixing commit SHA (for accepted issues) or a concise rationale (for declines). Resolve review threads when addressed or intentionally declined with rationale. Issue-level comments (those at `repos/.../issues/<N>/comments` rather than tied to a specific line) have no resolution action - acknowledge with a reply if needed and move on. - -After the final push on a PR, sweep older threads from earlier rounds whose code paths no longer exist; otherwise stale unresolved markers remain in the review UI. - -### Escalating to the User - -Bring the user in when: - -- **Genuine design trade-off** surfaces (fail-open vs fail-closed, narrow vs broad refactor scope, "should we add a guardrail or trust the docstring"). Triage, recommend, ask. -- **Repeated friction** across rounds without convergence - that's the rule-needs-updating signal. Stop, summarize the pattern, and let the user authorize the rule change. -- **Architectural redesign** is requested rather than a bug fix. Surface with a recommendation; never apply unilaterally. - -Anti-pattern: don't keep flipping the code on the same style point. Flip the rule once and stick to the rule. - -## Coding Conventions (Highlights) - -- Do not use `var`; use explicit types. -- File-scoped namespaces, Allman braces, and modern C# features are preferred. -- No `#region` usage. -- UTF-8 encoding without BOM. -- Respect line ending rules in `.editorconfig`. - -## Notes for Changes - -- When modifying Dockerfiles or build scripts, ensure generated outputs stay in sync with `CreateMatrix` behavior. -- Keep base image definitions and derived image Dockerfiles aligned, since derived images build on the base images. -- Keep `README.md` and release documentation aligned with build outputs and product variants. - -## Template adaptations - -This repo derives its CI and conventions from [ptr727/ProjectTemplate](https://github.com/ptr727/ProjectTemplate). Carried artifacts are taken by full-file replacement; the deliberate deviations below are documented so they are not mistaken for drift. - -- **Triggered-Docker publisher, one branch per run.** `publish-release.yml` is `workflow_dispatch` + weekly `schedule` (main only) + a path-scoped `push` to `main` on `Make/Matrix.json` (publishes a new codegen product pin at once). It builds exactly one branch - the trigger ref (`github.ref_name`) - so NBGV classifies natively with no cross-branch leg and no `IGNORE_GITHUB_REF`. The jobs are a single `get-version` -> `build-base` (main only) -> `build-docker` -> `github-release` (main only) -> `docker-readme` (main only) -> `cleanup-artifacts` chain. A develop dispatch refreshes the `:develop` images only (no GitHub release); the earlier two-leg `build-main` / `build-develop` combined run is removed. -- **Multi-image, shared-base build layer.** This is a multi-image Docker product: the shared `nx-base` / `nx-base-lsio` images are built once on the `main` run and reused by a develop dispatch (`build_base: false`, so it never overwrites the branch-agnostic `nx-base` tag), and `build-docker-task.yml` builds every product image (NxMeta, DWSpectrum, NxGo, WisenetWAVE, ...) from `Make/Matrix.json` (`max-parallel: 4`). The template's single-target branch matrix cannot express the shared-base fan-out, so the build layer stays repo-owned. -- **Single NBGV run threaded to the build task.** `build-docker-task.yml` has no nested `get-version`; the orchestrator's single `get-version` run threads `semver2` down as the image `LABEL_VERSION`, so one classification feeds every product leg (no second NBGV run can reclassify or collide a tag). -- **Docker-only GitHub release (no `release-asset-*` files).** The `github-release` job follows the template's generic release semantics (tag on the built commit, auto source zip + README + LICENSE, `target_commitish` pinned, skip-existing guard, main-only `Verify public release version` backstop), but this repo ships no binary/package release assets - the published artifacts are the Docker Hub images. So there is no `release-asset-*` download step and `fail_on_unmatched_files` is omitted (it has no files to guard). -- **Folded Docker Hub readme.** The standalone docker-readme task is removed; `publish-release.yml` carries `docker-readme-repos` + `docker-readme` jobs gated to `main` that derive the repository list inline from `Make/Matrix.json` (lowercased `ptr727/<image>` plus the shared base repos) and matrix `peter-evans/dockerhub-description` over it. -- **Dropped date badge.** The `build-datebadge-task.yml` workflow and its publisher job are removed, along with the README "Last Build" badge that pointed at the BYOB gist. -- **Husky.Net pre-commit hooks.** This repo installs Husky.Net Git hooks (pre-commit formatting/codegen), inverting the template's no-hooks default. The hooks run the same checks CI enforces, surfaced earlier. `validate-task.yml` (the rename of `test-release-task.yml`) runs the same Husky lint + `dotnet test` in CI as the required-check's quality gate. -- **.vscode Benchmark -> Husky.Net Run task.** The carried `.vscode/tasks.json` swaps the template's Benchmark task for a Husky.Net Run task, matching this repo's hook tooling. -- **Build-layer leaves own build specifics but follow the shared action-pin + cache rules.** The build-layer leaves (`build-docker-task.yml`, `build-base-images-task.yml`) own their per-image Dockerfiles, build args, and target matrix, but their actions are **SHA-pinned** like the orchestration layer (Dependabot still bumps SHA pins, updating the SHA + version comment), and their Docker layer cache uses **registry-tag caches** (`docker.io/ptr727/<repo>:buildcache-<branch>`, plus the base image's own tag and inline cache) rather than `type=gha`, per the template's cache policy. -- **No `merge-upstream-version` merge-bot job.** This repo tracks the upstream NX version through codegen (`run-codegen-pull-request-task.yml` updating `Make/Version.json` + `Make/Matrix.json`), so the merge-bot keeps `merge-codegen` and omits the template's `merge-upstream-version` job (it uses the separate `check-upstream-version-task.yml` mechanism this repo does not ship). +# Instructions for AI Coding Agents + +**NxWitness** builds and publishes Docker images for the Network Optix family of VMS products: Nx Witness, Nx Meta, Nx Go, DW Spectrum, and Wisenet WAVE, each in a standard and a LinuxServer.io variant. The published artifacts are exclusively the Docker Hub images; the C# in this repo is a codegen tool that tracks upstream product versions and writes the Dockerfiles, Compose files, and build matrix, and it is never packaged or published. + +This file is the entry point every coding agent reads first, and it holds only three things: the bootstrap that says where the canonical rules live and which procedure to follow for the state this repository is actually in, the rules for managing context and delegation, which apply to every task, and a map of where every other rule lives. The rule text itself is in [`GOVERNANCE.md`](./GOVERNANCE.md), one section per topic. Code style lives in [`CODESTYLE.md`](./CODESTYLE.md), the CI/CD workflow contract in [`WORKFLOW.md`](./WORKFLOW.md), this repository's own design in [`ARCHITECTURE.md`](./ARCHITECTURE.md), and how it is run, verified locally, and debugged in [`OPERATIONS.md`](./OPERATIONS.md). The table below maps a task to its rule section; a task about this repository's own design or operation goes to those two files instead. + +Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. This repository's project-specific conventions live in `ARCHITECTURE.md` and `OPERATIONS.md`, **not** in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), because that file targets GitHub Copilot / VS Code specifically, while this file and `GOVERNANCE.md` are the agent-agnostic ones every coding agent is directed to read, so any rule a reviewer must honor has to live in one of the agent-agnostic documents, meaning `GOVERNANCE.md` for a cross-cutting rule and `ARCHITECTURE.md` or `OPERATIONS.md` for one specific to this repository, to be provider-independent. This file is a router carrying three carried sections, so it is not itself a home for a new rule. + +## Fleet Bootstrap + +This repository is governed by a shared template, and the canonical rules, machine-readable spec, and procedures live in `github.com/ptr727/ProjectTemplate`, the repository these rules call the hub. Fetch that repository before acting on anything about conformance, carried content, repository settings, or standing a repository up, because a carried copy here can be stale or absent and the hub is the only authority on what this repository is supposed to hold. This section is byte-locked across every repository in the fleet, so it reads identically wherever it is found, and it is the entry point whenever nothing else present says where the rules are. + +Route by what this repository currently holds rather than by what it is expected to hold, since the two differ exactly when this section matters most. + +```mermaid +flowchart TD + state["what does this repository currently hold?"] + state -->|"no repo, or a local tree with no remote"| standup["hub STANDUP.md, from section 0"] + state -->|"no carried instruction set, or a partial one"| standup2["hub STANDUP.md sections 1A, 2"] + state -->|"instruction set present, current or stale"| resync["hub RESYNC.md"] + state -->|"believes it is conformant"| resync2["hub RESYNC.md, run the audit anyway"] +``` + +- **No repository yet, or a local tree with no remote.** Follow the hub's `STANDUP.md` from section 0. That file is hub-only and deliberately not carried, because a repository needing it cannot be relied on to hold a current copy. Note that nothing in it creates the GitHub repository, which is an outward-facing write requiring explicit permission, so section 0A is the list handed to the maintainer before anything else starts. +- **A repository with no carried instruction set, or a partial one.** Carry the baseline per the hub's `STANDUP.md` sections 1A and 2, which resolve what this repository is owed from its declared types and workflow model. Absent files are not drift to re-vendor, they are a baseline that never arrived, and the two are fixed differently. +- **A repository with the instruction set, current or stale.** Follow the hub's `RESYNC.md`, which runs `AUDIT.md` end to end for the findings and then applies each one in an order that matters, since the rules govern what comes after them, a deletion must precede the re-vendor that would otherwise refresh the file, and only some findings are mechanically detectable at all. An audit that reports drift and stops is half the procedure. +- **A repository that believes it is conformant.** Run the audit anyway and commit the report, because conformance asserted without a report is conformance nobody can check. This is the same procedure as the case above and is listed separately only because it is the one most often skipped. + +Three rules bound every path above. **Read the hub's `main` branch as ground truth**, since that is the promoted and gated state, and read `develop` only to detect divergence. **Reach the hub as a checkout of your own and fetch it immediately before reading it**, because a clone is whatever it last fetched rather than the branch it names, and work only in that checkout rather than in one that another task is using, per [`GOVERNANCE.md`](./GOVERNANCE.md) "Repository Boundaries and Write Safety" and "Hub-Hosted Tooling". And **the audit is read-only**: it produces a report and never edits the repository it measures, so a fix is a separate, reviewable change. + +## Context and Delegation Discipline + +An agent session is billed on the context it carries, not the work it does. Every request re-reads the whole accumulated context, so a token added early is paid for again on every request that follows, and a long session bills its last task for every earlier one. These are cost rules. None of them licenses doing less work, skipping verification, or shipping something unreviewed. + +### Session Scope + +- **One deliverable, one session.** A session covers one branch and one deliverable, and ends when that work merges. A multi-step task is one deliverable and stays in one session. Two unrelated tasks are two sessions even when they run back to back. +- **End a session at any of these, without being asked:** the branch changes, the pull request merges, or the next task is unrelated to the last. A review round is none of them. A loop still producing findings is the deliverable in progress, and a round count is not a reason to leave one open. +- **Hand off in a file, never in context.** Close a session by writing at most 2 KB to a scratch file: branch, pull request link, what is done, the next command. A summary held in context is re-billed until the session ends, and a summary on disk is read once by whoever needs it. +- **Re-derive state, do not carry it.** "This session already has the context" is the signal to split, not to continue. Context that has gone stale is worse than absent, because a file read hundreds of requests ago no longer describes the file. +- **Compaction is a fallback, not the strategy.** It restarts context from a floor and climbs again, where a fresh session starts from zero. + +### Reading + +- **Map a large file, then read one range.** For anything over about 200 lines, list the headings with `grep -n '^## '` first and read only the range the task needs. Read the section, not the file that contains it. +- **Prefer an in-place edit to a whole-file rewrite.** Rewriting a file bills its full content again on top of what the read already cost. + +### Commands + +- **Bound output at the source.** Write every command so its output is the answer, not the haystack: a `--jq` projection on an API call, a count or files-only flag on a search, a summary flag on a diff, an explicit cap on anything unbounded. A command whose output you then skim is a command that should have been narrower. +- **Keep a long query in a file, not in the command.** A heredoc re-typed on every call costs its own length in context each time, often more than the answer it retrieves. +- **Keep generated caches outside the checkout when the executor restricts writes.** Give each task a cache directory under a writable temporary root. Point tools such as uv and ruff there through their own cache variables. Never repurpose `HOME` or an agent's configuration directory to make a tool run. +- **Report an execution boundary separately from a check finding.** A denied path, network request, or Docker socket says the check did not run. Preserve that failure, then use the executor's approval mechanism for the required rerun. Request the narrowest reusable command prefix the executor supports. Report the rerun's result as the verification evidence. + +### Delegation + +- **Delegate exploration, keep judgment.** A subagent starts from an empty context and returns only its conclusion, so a wide search, a multi-file audit, or a "which of these is affected" question costs a fraction of the same work inline. Delegate when the finding compresses to a short answer, and stay inline when the intermediate detail drives the next edit. +- **Match the model tier to the judgment, not to the diff size.** Mechanical work (a known-shape edit repeated across files, an extraction, a status check, a lint fix) runs on the cheapest model that does it correctly, at the lowest reasoning effort that holds. State the tier in the delegation itself rather than accepting the default. A change to a gate, a ruleset, a release condition, or a carried governance section is a design change however small it looks. +- **Never tier down the seat holding the judgment.** Governance wording, spec logic, rulesets, repository visibility, and the decision to decline a review finding are fleet-wide and durable when wrong. Tier the subagents, not the main thread. +- **Brief a subagent so it never needs a governance file.** A subagent inherits no context, so anything it must honor has to be in its prompt. Reading `GOVERNANCE.md` to find out costs it the same tokens the main thread would have paid. Brief on this shape: + +```text +Task: <the one question or edit, stated so the answer compresses> +Paths: <exact files or globs - never "find the relevant files"> +Rules that bind this task: <the specific rules, quoted, not a pointer to a doc> +Return: <the shape of the answer - a list, a diff, a yes/no with evidence> +Bounds: <what not to touch, and what to do when a rule looks incomplete> +If a rule you were given does not cover what you find, stop and report it. Do not guess, and do not read a governance file to resolve it. +``` + +- **Wait in a background process, not in a poll loop.** A review or CI wait is a sequence of near-identical requests, each billed for whatever context it happens to carry. Run the wait as one backgrounded command that returns when the condition is met. +- **A wait separates three outcomes, and says which one it reached.** The condition was met, it has not been met yet, and the wait cannot reach it at all are three different results, and a backgrounded wait that emits nothing renders all three identically. Run the command once in the foreground and read its output before backgrounding it, because a wait is only as good as the command inside it, and an unsupported flag on the installed tool version exits non-zero with an empty stdout that every naive test reads as "nothing yet". Never let a fallback stand in for a failed command, since `|| echo '[]'`, `|| true`, and `2>/dev/null` convert an error into that same reading, which is the suppression the write-safety rules already forbid on a mutation. Make the wait emit on failure as loudly as on success, so silence means "still running" and nothing else, and bound it, so a condition that is never coming ends in a report rather than in another wait. + +## Where the Rules Live + +Every rule below is a level-two section of [`GOVERNANCE.md`](./GOVERNANCE.md). Read the section the task needs. + +| Working on | Section | +| --- | --- | +| Why the rules are shaped this way | `Foundational Principles` | +| Recording a durable lesson or updating governance | `Durable Knowledge and Self-Improvement`, surfaced at its decision moment by the `agent-conduct` Skill, and the section keeps the full rules | +| Any push, API mutation, comment, label, or merge, or which checkout the work happens in | `Repository Boundaries and Write Safety`, its task-isolation rule surfaced at the task-start moment by the `repo-worktree` Skill, and the section keeps the full rules | +| Quoting data into a comment, commit, test, or doc | `Representative Data in Agent-Authored Text` | +| Committing, signing, rebasing, force-pushing | `Git and Commit Rules`, packaged as the `git-commit-conventions` Skill | +| Branch choice, promotion, keeping branches in sync | `Branching Model`, packaged as the `operational-vs-release-workflow` Skill | +| Releasing, version bumps, publishing | `Release Model`, packaged as the `operational-vs-release-workflow` Skill | +| A live config repo rather than a code repo | `Operational Repositories`, packaged as the `operational-vs-release-workflow` Skill | +| Onboarding a repo or running a conformance sweep | `Repository Onboarding and Conformance` (hub only, not carried). Standing up a new repo from a hub checkout is packaged as the `standup-a-repo` Skill, resyncing one already stood up the same way is `resync-a-repo`, and measuring a named repo against the fleet ground truth per `AUDIT.md` is `audit-a-repo`, all hub-context only | +| Running a fleet gate, the review digest, or the config script | `Hub-Hosted Tooling` | +| Running a lint or format check locally, or a lint tool missing from `command -v` | `Running the Linters Locally (Known-Working Invocations)` | +| Running a test locally, or a test runner missing or failing to spawn | `Verification Discipline` | +| Writing a commit message or pull request title | `Pull Request Title and Commit Message Conventions`, packaged as the `comment-and-doc-style` Skill | +| Any prose, comment, doc, or line-ending change | `Documentation Style Conventions`, packaged as the `comment-and-doc-style` Skill | +| Proving work actually happened | `Verification Discipline`, surfaced at its decision moment by the `agent-conduct` Skill, and the section keeps the full rules | +| Opening a pull request, or requesting, monitoring, answering, or closing a review | `PR Review Etiquette`, packaged as the `pr-review-conduct` Skill | +| Reviewing a pull request, patch, or change set | `code-review`, which routes to the applicable general, language, documentation, and workflow skills | +| Reporting progress or asking the user something | `Communicating with the User`, surfaced at its decision moment by the `agent-conduct` Skill, and the section keeps the full rules | +| Editing a workflow YAML file | `Workflow YAML Conventions`, surfaced with the full `WORKFLOW.md` contract by the `workflow-ci-contract` Skill, and this section and `WORKFLOW.md` keep the full rules | +| Choosing an OS, runtime, or toolchain target | `Supported Development Platforms` | +| The devcontainer | `Devcontainer` | +| Editor settings and tasks | `Editor and Tasks` | +| The About panel, description, or repo toggles | `Repository Details` | +| Where a file belongs in the tree | `Repository Layout` | + +A row above with no Skill annotation is doc-only by decision, not by omission. A Skill surfaces rules at a trigger moment, and each unannotated section either binds always or carries no moment narrower than reading it: `Foundational Principles` is rationale read once rather than a procedure, `Repository Boundaries and Write Safety` and `Representative Data in Agent-Authored Text` are always-on law that must bind even when no Skill fires (the `gh-write-guard` hook and the host-wide instruction blocks the agent-safety installer maintains are their enforcement layer, and the one moment in the boundaries section narrow enough to surface, isolating into a worktree at task start, gets the `repo-worktree` Skill on top of that law rather than instead of it), and `Hub-Hosted Tooling`, `Supported Development Platforms`, `Devcontainer`, `Editor and Tasks`, `Repository Details`, and `Repository Layout` are short reference sections a task reads at the moment it touches their subject, each already routed to by the procedures and Skills that need it. + +Some of the rules above are also packaged as Claude Code / opencode / Codex Skills, hand-authored at `.agents/skills/` in the hub (not a repo-relative link here, since that path is hub-local and not carried into every fleet repo), so they surface automatically instead of needing to be re-read every session. `scripts/` is hub-hosted and reached rather than carried, per "Hub-Hosted Tooling", so run the installer from a hub checkout: `python3 scripts/skills_install.py` (or the `.sh`/`.ps1` wrapper) once per machine, from `github.com/ptr727/ProjectTemplate`, installs them for every repo touched from that machine. `python3 scripts/skills_install.py --report`, also from a hub checkout, says whether this machine is current. A rule that keeps needing to be restated is a sign the install is missing or stale, not that the rule does not exist. Keeping a repo's own carried `.github/copilot-instructions.md` in sync with the hub, without losing that repo's own "Disproved Claims" ledger entries in the process, is `copilot-instructions-keeper`, a skill about maintaining that file rather than a rule extracted from it, since the file itself is read directly by the Copilot bot and stays fully intact everywhere it is carried. Checking, from inside this repo's own session with no operator watching, whether this repo and this machine are actually current against the hub is `fleet-conformance-check`, new content rather than a rule extracted from a section, the counterpart to `resync-a-repo` that needs no standing hub checkout or named target beyond the repo the session is already in, even though its own check fetches a hub checkout to reach `scripts/skills_install.py`. Opening a pull request against a repository outside this fleet, one the maintainer does not control, follows a different workflow entirely, new content rather than a rule extracted from a section, packaged as `upstream-contribution-workflow` and independent of the target repo's own type or workflow model. Isolating a task into its own worktree before its first file edit, with the base-branch choice, the layout convention, and the cleanup mechanics, is `repo-worktree`, the task-start surface of the `Repository Boundaries and Write Safety` law, which keeps the rule. Creating, changing, or retiring one of these skills is itself packaged as `skill-lifecycle`, hub-context only, since `.agents/skills/` exists only in the hub and the generated plugin tree is never hand-edited. + +Adding or changing a managed host tool is packaged as `add-host-tool`. It keeps the cross-platform contract, installer, documentation, test, and native-verification surfaces together. + +Driving a pull request through its review loop, from a feature branch into `develop` and, when asked, on to a mergeable `develop -> main` promotion PR, disposing of every reviewer finding along the way per `pr-review-conduct`, is packaged as `drive-pr`, new content rather than a rule extracted from a section. Merging a ready promotion PR and dispatching the release it unblocks, refreshing this machine's installed Skills first when the repo is this hub, is `merge-and-release`, its own new-content package, invoked separately from `drive-pr` so the promotion merge and the release dispatch each keep their own explicit go-ahead. + +Running one read-only, adversarial review pass against a branch's current diff against its target branch, full file context included, on the strongest model tier the session can reach, before a unit of PR-bound work is pushed toward a pull request or claimed done, is packaged as `local-strict-review`, new content rather than a rule extracted from a section. `drive-pr`, `pr-review-conduct`, and `agent-conduct` each reference it at the moment they already govern, rather than restating what it does. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..eb5d5c2c --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,77 @@ +# Architecture + +How this repo is built: what it publishes, how the codegen data flows from an upstream product version to a built image, how the base and derived images relate, and which of its structures are deliberate deviations from the fleet template rather than drift. + +## Products and Published Artifacts + +This repository builds and publishes Docker images for Network Optix VMS products: Nx Witness, Nx Meta, Nx Go, DW Spectrum, and Wisenet WAVE. Each product ships in two variants, a plain Ubuntu image and a LinuxServer.io (LSIO) image, giving ten product images built on top of two shared base images, `nx-base` and `nx-base-lsio`. + +There is **no NuGet publish**. `CreateMatrix` is a build-time code generator, not a shipped package, and the published artifacts are exclusively the Docker Hub images. + +## Codegen Data Flow + +The build inputs are generated rather than hand-maintained, and the generator is the .NET side of the repo: + +- `CreateMatrix` is a .NET 10 console app. It fetches product release metadata from the upstream Network Optix release feeds and writes this repo's build inputs. +- `CreateMatrixTests` is its xUnit v3 test project, using AwesomeAssertions. + +One upstream version becomes a built image along this path: + +1. `CreateMatrix version` reads the upstream release feeds and refreshes `Make/Version.json`, which holds, per product, the version list with its x64 and arm64 download URIs and its labels (`Stable`, `Latest`, `RC`, `Beta`). +2. `CreateMatrix matrix` expands `Make/Version.json` into `Make/Matrix.json`, the build matrix. Each row is exactly one image build, carrying its image `Name`, `Product`, `Branch` (`main` or `develop`), `Base` (`ubuntu` or `lsio`), the Docker Hub `Tags` it publishes, and the `Args` (download URLs and version) the build consumes. +3. `CreateMatrix make` renders the per-product Dockerfiles into `Docker/` and the test Compose files into `Make/` from those inputs. +4. CI builds `Make/Matrix.json` row by row, so a product version reaches an image only by being pinned in that file first. + +`Make/Matrix.json` is therefore the pin: it is what the publisher reads, what the codegen pull request updates, and what a path-scoped push to it publishes. `version.json` at the repo root is unrelated to product versions; it is the Nerdbank.GitVersioning input. + +Because `Docker/` and the Compose files under `Make/` are generated, a change to the generator is incomplete until the regenerated output is committed alongside it. Keep generated outputs in sync with `CreateMatrix` behavior, and keep `README.md` and the release documentation aligned with the build outputs and product variants. + +## Image Architecture + +- The base images `nx-base` and `nx-base-lsio` are hand-written (`Docker/NxBase.Dockerfile`, `Docker/NxBase-LSIO.Dockerfile`). They are built and pushed first, then reused as the `FROM` image for every derived product Dockerfile. +- The base tag is branch-agnostic (`nx-base:ubuntu-noble` and `nx-base-lsio:ubuntu-noble`), so it is built once on the `main` publish run and reused rather than rebuilt by a develop run, which would otherwise overwrite it. +- Derived product images stay aligned with base image changes and tags, the Ubuntu distro tag in particular. A base change that a derived image is not aligned to is the failure this rule exists to prevent, and nothing in the pull request pipeline catches it because the pull request smoke build covers only two of the ten product images. +- The two variants differ only in their base and the user model that follows from it. `nx-base` is built from `ubuntu:noble`, and `nx-base-lsio` is built from `lsiobase/ubuntu:noble` and follows the LinuxServer.io user conventions (`Docker/lsio-rename-user.sh`). The s6-overlay service tree under `Docker/s6-overlay` and the shared scripts `Docker/download.sh` and `Docker/entrypoint.sh` are common to both. +- Every image is built for `linux/amd64` and `linux/arm64`. + +## CI Pipeline (GitHub Actions) + +The full CI/CD contract, meaning triggers, jobs, the one-branch publish model, versioning, and the multi-image build layer, is specified in [WORKFLOW.md][workflow], the canonical guide. The summary below is a pointer and does not duplicate those rules. + +- CI runs on **push to every branch** ([test-pull-request.yml][test-pull-request]): it validates ([validate-task.yml][validate-task]) on every push, and runs a fast smoke build ([build-docker-task.yml][build-docker-task] with `smoke: true`, meaning NxMeta and NxMeta-LSIO, amd64, no push) only when image files (`Docker/**`, `Make/Matrix.json`, `Make/Version.json`) change, via an inline `git diff` change-gate. One aggregator job, `Check pull request workflow status job`, is the ruleset-bound required check. +- Publishing is **triggered-Docker, one branch per run** ([publish-release.yml][publish-release]): the triggers are the weekly schedule (rebuilds `main` only), a path-scoped push to `main` on `Make/Matrix.json` (publishes a new codegen product pin at once), and manual dispatch (publishes the started-from branch). One run computes the version once ([get-version-task.yml][get-version-task]), builds the shared base once (main only, with a develop dispatch reusing it via `build_base: false`), builds the full product matrix from `Make/Matrix.json`, and on `main` cuts the GitHub release and pushes the Docker Hub overviews. +- Merges to `main` or `develop` do not build or publish images by themselves. Only the matrix-pin push to `main`, a schedule, or a dispatch publishes. Auto-merged Dependabot and codegen pull requests land commits the next publish picks up. +- Do not reintroduce a two-branch publish matrix, a nested `get-version` in the build task, the date-badge or standalone docker-readme workflows, or `dorny/paths-filter`. + +## Versioning Labels and Tags + +Two version streams reach the images, and confusing them is the common error: + +- **The NBGV version labels the image.** The orchestrator's single `get-version` run computes the Nerdbank.GitVersioning version once and threads its `semver2` output down into `build-docker-task.yml` as the `LABEL_VERSION` build arg, so one classification labels every product leg and no second NBGV run can reclassify it. The same version is the GitHub release tag on `main`. +- **The Nx product version tags the image.** The Docker Hub tags come from `Make/Matrix.json`, not from NBGV. A row's `Tags` carry the upstream product version and its channel alias (`stable`, `latest`, `rc`, `beta`, and the `develop-` prefixed forms). The NBGV version appears only as the image label and the release tag, never as a Docker tag. + +## Template Adaptations + +This repo derives its CI and conventions from the fleet template, `ptr727/ProjectTemplate`. Carried artifacts are taken by full-file replacement, and the deliberate deviations below are documented so they are not mistaken for drift. + +- **Triggered-Docker publisher, one branch per run.** `publish-release.yml` is `workflow_dispatch` plus a weekly `schedule` (main only) plus a path-scoped `push` to `main` on `Make/Matrix.json`. It builds exactly one branch, the trigger ref (`github.ref_name`), so NBGV classifies natively with no cross-branch leg and no `IGNORE_GITHUB_REF`. The jobs are a single `get-version` -> `build-base` (main only) -> `build-docker` -> `github-release` (main only) -> `docker-readme` (main only) -> `cleanup-artifacts` chain. A develop dispatch refreshes the `:develop` images only, with no GitHub release, and there is no two-leg `build-main` / `build-develop` combined run. +- **Multi-image, shared-base build layer.** The shared `nx-base` and `nx-base-lsio` images are built once on the `main` run and reused by a develop dispatch (`build_base: false`, so it never overwrites the branch-agnostic `nx-base` tag), and `build-docker-task.yml` builds every product image from `Make/Matrix.json` with `max-parallel: 4`. The template's single-target branch matrix cannot express the shared-base fan-out, so the build layer stays repo-owned. +- **Single NBGV run threaded to the build task.** `build-docker-task.yml` has no nested `get-version`. The orchestrator's single `get-version` run threads `semver2` down as the image `LABEL_VERSION`, so one classification feeds every product leg and no second NBGV run can reclassify or collide a tag. +- **Docker-only GitHub release, with no `release-asset-*` files.** The `github-release` job follows the template's generic release semantics (tag on the built commit, auto source zip plus README and LICENSE, `target_commitish` pinned, skip-existing guard, main-only `Verify public release version` backstop), but this repo ships no binary or package release assets because the published artifacts are the Docker Hub images. So there is no `release-asset-*` download step, and `fail_on_unmatched_files` is omitted since it has no files to guard. +- **Folded Docker Hub readme.** There is no standalone docker-readme task. `publish-release.yml` carries `docker-readme-repos` and `docker-readme` jobs gated to `main` that derive the repository list inline from `Make/Matrix.json` (lowercased `ptr727/<image>` plus the shared base repos) and matrix `peter-evans/dockerhub-description` over it. +- **No date badge.** The repo ships no `build-datebadge-task.yml` workflow and no publisher job for it, and `README.md` carries no "Last Build" badge pointing at a BYOB gist. +- **Husky.Net pre-commit hooks.** This repo runs its local hook through Husky.Net, configured in `.husky/task-runner.json`, rather than through the Python `pre-commit` framework. The hook runs the same CSharpier and `dotnet format style` checks CI enforces, surfaced earlier, and `validate-task.yml`, which stands in for the template's `test-release-task.yml`, runs that same Husky lint plus `dotnet test` in CI as the required check's quality gate. +- **Repo-owned build-layer leaves.** The build-layer leaves (`build-docker-task.yml`, `build-base-images-task.yml`) own their per-image Dockerfiles, build args, and target matrix, which the template's build layer does not express. Owning those specifics is not an exemption from the shared workflow conventions: their actions are SHA-pinned like the orchestration layer, and their Docker layer cache uses per-image registry-tag caches (`docker.io/ptr727/<repo>:buildcache-<branch>`, plus the base image's own tag and inline cache). +- **No `merge-upstream-version` merge-bot job.** This repo tracks the upstream Nx version through codegen, with `run-codegen-pull-request-task.yml` updating `Make/Version.json` and `Make/Matrix.json`, so the merge bot keeps `merge-codegen` and omits the template's `merge-upstream-version` job, which uses the separate `check-upstream-version-task.yml` mechanism this repo does not ship. + +The `.vscode` task-set deviation is recorded in [OPERATIONS.md][operations], with the tooling it belongs to. + +<!-- Repo --> + +[build-docker-task]: ./.github/workflows/build-docker-task.yml +[get-version-task]: ./.github/workflows/get-version-task.yml +[operations]: ./OPERATIONS.md +[publish-release]: ./.github/workflows/publish-release.yml +[test-pull-request]: ./.github/workflows/test-pull-request.yml +[validate-task]: ./.github/workflows/validate-task.yml +[workflow]: ./WORKFLOW.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a2c84a14 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# Claude Code Entry Point + +@AGENTS.md + +Claude Code reads `CLAUDE.md`, not `AGENTS.md`, so the import line above is what gets this repository's rules into a Claude Code session at all. See `AGENTS.md` for what is authoritative and why. This file carries no rule of its own, and adds none beyond the import line. diff --git a/CODESTYLE.md b/CODESTYLE.md index ebb29ffb..c610f222 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -1,464 +1,468 @@ -# Code Style and Formatting Rules - -This is the single code-style guide for the repo. The **General** section applies to every language. Each **language section** (.NET, Python) is self-contained: a repo reads only the section(s) for the languages it ships and ignores the rest. The whole file is carried, not trimmed - an unused-language section costs nothing and keeps re-sync a clean overwrite, the same carry-whole model as [`.editorconfig`](./.editorconfig), whose inert `[*.cs]` block a non-.NET repo keeps. - -Cross-cutting *process* rules (PR titles, branching, US English, markdown style, comments philosophy, workflow YAML, PR review etiquette) live in [AGENTS.md](./AGENTS.md) and are not repeated here. - -## General - -These rules apply to every language in the repo. - -### Tooling Names and Casing - -Use each tool's official casing in task labels, docs, and prose - `.NET` (not `.Net`), `CSharpier`, `ruff`, `pyright`, `uv`. Don't invent personal variants. - -### Clean-Compile Verification - -Each language defines a **clean-compile** verification - the combination of build, formatter, linter, and code-analysis tools that must report clean before a commit. It is exposed as one or more **named** VS Code tasks (or, where a language ships no tasks, documented commands), and those definitions are **carried verbatim** across derived repos. The concrete names live in each language section below. - -- **Run it after every code change.** The relevant language's clean-compile must pass before you commit; CI runs the same checks as a backstop. -- **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly; either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A local commit/pre-commit gate is the repo's choice.** No single hook runner fits every language (a `dotnet`-tool runner like Husky.Net suits .NET but not Python), so none is mandated - but that is **not** a recommendation against commit gates. CI is the authoritative backstop regardless; a local gate is an additive convenience a repo may wire and keep - Husky.Net (and `dotnet husky run` as a style step) for .NET, `pre-commit` for Python. Keeping a working gate is not drift. - -### Analyzer Diagnostics and Suppressions - -- **A new port is not a license to silence diagnostics.** Brownfield / just-ported status never justifies relaxing analyzer or linter severities or muting newly surfaced warnings - fix them. (The only brownfield allowance in this template is the one-time git-signing / line-ending migration described in [AGENTS.md](./AGENTS.md) and [README.md](./README.md), which has nothing to do with code analysis.) -- **Suppress only genuine false-positives or deliberate, documented exceptions**, always at the **narrowest scope that fits**, in this order of preference: - 1. An **in-code annotation on the specific symbol**, with a justification - the language's attribute/comment form, never a blanket pragma spanning a region. - 2. The **owning project's local config** when the exception is project-wide for one project (e.g. a test project's own `.editorconfig` / `pyproject.toml`). - 3. The **root / shared config** only when the suppression is genuinely applicable to **every** project in the repo. -- **Never blanket-relax a batch of rules project-wide** to get a port to build. The per-language mechanics (which attribute, which config key) are in each language section. - -### Markdown and Spelling - -These apply repo-wide, in every directory: - -1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc) at the repo root is the single source of truth - the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length, `MD033` inline HTML) are **intentional** - do not "fix" them. Fix violations at the source rather than disabling rules. -2. **Spelling**: All spelling must be clean via the CSpell VS Code integration; words must be correctly spelled in **US English** (the repo-wide convention - see [AGENTS.md](./AGENTS.md)). Project-specific terms go in the workspace CSpell config. -3. **Spelling CI scope**: The enforced CI spell-check gate covers **`README.md` and `HISTORY.md` only** - these are the files every repo visitor sees, so they must be clean. It is deliberately **not** all `**/*.md`: repos carry many markdown files full of technical terms, and gating every one of them would mean endlessly padding `cspell.json` just to keep CI green. Broad, live spell-checking across any file (source, markdown, text) is the **cspell editor extension's** job, so typos still surface to whoever is editing. A repo owner **may** widen their own CI file list, but the template ships README + HISTORY as the default; keep every surface that runs cspell - the CI workflow and any local VS Code task or one-liner the repo has - on the same file list. The list is explicit (not a glob), so a repo that ships no `HISTORY.md` (e.g. one with no changelog) must drop it from all three surfaces and gate on `README.md` alone - cspell errors on a listed file that does not exist. Markdown *linting* (item 1) stays repo-wide `**/*.md` - it does not choke on technical terms. - -## .NET - -*This section applies only to the .NET side. A repo with no .NET projects still carries it (the file is carried whole) and ignores it.* - -This is the style guide for any **.NET projects** in this repo. - -### Build Requirements - -#### Zero Warnings Policy - -**CRITICAL**: All builds must complete without warnings. The project enforces this through: - -1. **The `.NET Format` clean-compile task** (see [Clean-Compile Verification](#clean-compile-verification)) - - The .NET clean-compile is the **`.NET Format`** VS Code task, which chains `CSharpier Format` -> `.NET Build` -> `dotnet format style --verify-no-changes`. These three task definitions are carried verbatim in [`.vscode/tasks.json`](./.vscode/tasks.json). - - After any code change it must pass before commit. Run the `.NET Format` task. To run it natively instead, reproduce that task chain from [`.vscode/tasks.json`](./.vscode/tasks.json) exactly - `CSharpier Format`, then `.NET Build`, then the `dotnet format style --verify-no-changes --severity=info ...` verify - without dropping or loosening any argument (tasks.json is the canonical command spec). Bare `dotnet format` alone, skipping CSharpier or the build, is not sufficient. - -2. **Analyzer configuration** - - `<EnableNETAnalyzers>true</EnableNETAnalyzers>` with `<AnalysisLevel>latest-all</AnalysisLevel>` and `<AnalysisMode>All</AnalysisMode>` (full analyzer set enabled) - - `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` - any diagnostic surfaced as a warning fails the build, so it must be fixed or deliberately suppressed, not left to accumulate (see [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions)) - -3. **CI lint backstop** - - CI runs the clean-compile checks on every PR as the authoritative backstop - - Git hooks are optional; a repo may wire a local runner (Husky.Net) for pre-commit enforcement, but CI is the gate that matters - -#### Build Tasks - -Available VS Code tasks (run them from VS Code's task runner - **Terminal -> Run Task** - or an agent's task-running tool). The three clean-compile tasks below are carried verbatim; a repo adds its own convenience tasks (tool updates, dependency upgrades, benchmarks) on top: - -- `.NET Build`: Build with diagnostic verbosity *(clean-compile)* -- `CSharpier Format`: Auto-format code with CSharpier *(clean-compile)* -- `.NET Format`: Run CSharpier and build, then verify formatting and style with `--verify-no-changes` *(clean-compile; the task to run after edits)* - -### Tooling and Editor - -#### Code Formatting and Tooling - -1. **CSharpier**: Primary code formatter - - Invoked by the `CSharpier Format` task / `dotnet csharpier format --log-level=debug .` -2. **dotnet format**: Style verification - - Verify no changes: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed` -3. **Other tools** - - `dotnet-outdated-tool`: Dependency update checks - - Nerdbank.GitVersioning: Version management - -CI is the authoritative lint backstop. Local pre-commit hooks are optional - wire Husky.Net (or another runner) if you want local enforcement. - -#### Editor Baseline - -1. **Required VS Code extensions**: CSharpier, markdownlint, CSpell -2. **VS Code settings**: Use the workspace settings without overrides - -### Coding Standards and Conventions - -Note: Code snippets are illustrative examples only. Replace namespaces/types to match your project. - -#### C# Language Features - -1. **File-scoped namespaces** - - ```csharp - namespace Example.Project.Library; - ``` - -2. **Nullable reference types**: Enabled (`<Nullable>enable</Nullable>`) - - Use nullable annotations appropriately - - Use `required` for mandatory properties - -3. **Modern C# features**: Prefer modern language constructs - - Primary constructors when appropriate - - Top-level statements for console apps - - Pattern matching over traditional checks - - Collection expressions when types loosely match - - Extension methods - the classic `this`-parameter form, or an `extension(<receiver>) { ... }` block on C# 14+ - - Implicit object creation when type is apparent - - Range and index operators - -4. **Expression-bodied members**: Use for applicable members - - Methods, properties, accessors, operators, lambdas, local functions - -5. **`var` keyword**: Do NOT use `var` (always use explicit types) - - ```csharp - // Correct - int count = 42; - string name = "test"; - - // Incorrect - var count = 42; - var name = "test"; - ``` - -#### Naming Conventions - -1. **Private fields**: underscore prefix with camelCase - - ```csharp - private readonly HttpClient _httpClient; - private int _counter; - ``` - -2. **Static fields**: `s_` prefix with camelCase - - ```csharp - private static int s_instanceCount; - ``` - -3. **Constants**: PascalCase - - ```csharp - private const int MaxRetries = 3; - ``` - -#### Code Structure - -1. **Global usings**: Use `GlobalUsings.cs` for common namespaces - - ```csharp - global using System; - global using System.Net.Http; - global using System.Threading.Tasks; - global using Serilog; - ``` - -2. **Usings placement**: Outside namespace, sorted with `System` directives first - - ```csharp - using System.CommandLine; - using System.Runtime.CompilerServices; - using Example.Project.Library; - - namespace Example.Project.Console; - ``` - -3. **Braces**: Allman style - - ```csharp - public void Method() - { - if (condition) - { - // code - } - } - ``` - -4. **Indentation** - - C# files: 4 spaces - - XML/csproj files: 2 spaces - - YAML files: 2 spaces - - JSON files: 4 spaces - -5. **Line endings** - - C#, XML, YAML, JSON, Windows scripts: CRLF - - Linux scripts (`.sh`): LF - -6. **`#region`**: Do not use regions. Prefer logical file/folder/namespace organization. -7. **Member ordering (StyleCop SA1201)**: const -> static readonly -> static fields -> instance readonly fields -> instance fields -> constructors -> public (events -> properties -> indexers -> methods -> operators) -> non-public in same order -> nested types - -#### Comments and Documentation - -1. **XML documentation** - - `<GenerateDocumentationFile>true</GenerateDocumentationFile>` - - Missing XML comments for public APIs are suppressed (`.editorconfig`) - - Must document all public surfaces. - - Single-line summaries, additional details in remarks, document input parameters, return values, exceptions, and add crefs - - ```csharp - /// <summary> - /// Example of a single line summary. - /// </summary> - /// <remarks> - /// Additional important details about usage. - /// Multiple lines if needed. - /// </remarks> - /// <param name="category"> - /// The quote category to request - /// </param> - /// <param name="cancellationToken"> - /// A <see cref="System.Threading.CancellationToken"/> that can be used to cancel the request. - /// </param> - /// <returns> - /// A <see cref="string"/> containing the quote text. - /// </returns> - /// <exception cref="System.ArgumentException"> - /// Thrown when <paramref name="category"/> is not a supported value. - /// </exception> - public async Task<string> GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) {} - ``` - -#### Analyzer Suppressions (.NET) - -Follow the scope hierarchy in [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions). .NET mechanics, narrowest first: - -- **Never use `#pragma warning disable`** to silence an analyzer. -- **Symbol-scoped**: a `[System.Diagnostics.CodeAnalysis.SuppressMessage(...)]` attribute with a `Justification`, on the specific member or type: - - ```csharp - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Design", - "CA1034:Nested types should not be visible", - Justification = "https://github.com/dotnet/sdk/issues/51681" - )] - ``` - -- **Project-scoped** (e.g. a test project): a `dotnet_diagnostic.<RULE>.severity` entry in *that project's own* `.editorconfig`, with a comment explaining why. -- **Repo-wide**: a `dotnet_diagnostic.<RULE>.severity` entry in the root `.editorconfig`, only when the rule is genuinely not applicable to any project. Relaxing a batch of `CA*` rules (or `dotnet_analyzer_diagnostic.severity`) to push a brownfield port through the build is exactly what this forbids. - -#### Error Handling and Logging - -1. **Serilog logging**: Use structured logging - - ```csharp - logger.Error(exception, "{Function}", function); - ``` - -2. **Library log configuration**: Libraries must expose logging configuration - - Provide options or settings to supply an `ILoggerFactory` and/or `ILogger` - - Offer a global fallback logger for static usage when needed - -3. **CallerMemberName**: Use for automatic function name tracking - - ```csharp - public bool LogAndPropagate( - Exception exception, - [CallerMemberName] string function = "unknown" - ) - ``` - -4. **Logger extensions**: Use `Extensions.cs` for logger and other extension methods - - ```csharp - extension(ILogger logger) - { - public bool LogAndPropagate(Exception exception, ...) { } - } - ``` - -5. **Exceptions**: Do not swallow exceptions; log and rethrow or translate to a domain-specific exception - -#### Code Patterns - -1. **Guard clauses**: Prefer early returns for validation and error handling -2. **Async all the way**: Avoid blocking calls (`.Result`, `.Wait()`); use `async`/`await` -3. **Cancellation tokens**: Accept `CancellationToken` as the last parameter and pass it through -4. **ConfigureAwait**: In library code, use `ConfigureAwait(false)` unless context is required - - Do not call `ConfigureAwait(false)` in xUnit tests (see xUnit1030) -5. **Disposables**: Use `await using` for async disposables; prefer `using` declarations -6. **LINQ vs loops**: Use LINQ for clarity, loops for hot paths or allocations -7. **HTTP**: Reuse `HttpClient` via factory; avoid per-request instantiation -8. **Collections**: Prefer `IReadOnlyList<T>`/`IReadOnlyCollection<T>` for public APIs -9. **Immutability**: Prefer immutable records; use init-only setters when records are not suitable; prefer immutable or frozen collections for read-only data -10. **Exceptions as control flow**: Avoid using exceptions for expected flow -11. **Sealing classes**: Seal classes that are not designed for inheritance -12. **Read-only data**: Use immutable or frozen collections for read-only data sets -13. **Lazy initialization**: Use `Lazy<T>` for static, thread-safe instantiation (e.g., logger factory, HTTP factory) - -#### Testing Conventions - -1. **Framework**: xUnit with AwesomeAssertions - - ```csharp - [Fact] - public void MethodName_Scenario_ExpectedBehavior() - { - // Arrange - int expected = 42; - - // Act - int actual = GetValue(); - - // Assert - actual.Should().Be(expected); - } - ``` - -2. **Organization**: Arrange-Act-Assert pattern -3. **Naming**: Descriptive names with underscores -4. **Theory tests**: Use `[Theory]` with `[InlineData]` - -### Project Configuration - -1. **Target framework**: .NET 10.0 (`<TargetFramework>net10.0</TargetFramework>`) - -2. **AOT compatibility** - - `<IsAotCompatible>true</IsAotCompatible>` - - `<VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility>` - -3. **Assembly information** - - Use semantic versioning - - Include SourceLink: `<PublishRepositoryUrl>true</PublishRepositoryUrl>` - - Embed untracked sources: `<EmbedUntrackedSources>true</EmbedUntrackedSources>` - -4. **Internal visibility**: Use `InternalsVisibleTo` for test and benchmark access (adapt the project names to your repo's test/benchmark projects) - - ```xml - <ItemGroup> - <InternalsVisibleTo Include="YourBenchmarkProject" /> - <InternalsVisibleTo Include="YourTestProject" /> - </ItemGroup> - ``` - -### Best Practices - -1. **Code reviews**: All changes go through pull requests - -## Python - -*This section applies only to the Python side. A repo with no Python projects still carries it (the file is carried whole) and ignores it.* - -This is the style guide for any **Python project(s)** in this repo. - -### Toolchain - -| Tool | Role | Config | -|---|---|---| -| [uv](https://docs.astral.sh/uv/) | env, deps, build, publish | `pyproject.toml` `[dependency-groups]`, `uv.lock` | -| [hatchling](https://hatch.pypa.io/latest/) | build backend | `pyproject.toml` `[build-system]` | -| [ruff](https://docs.astral.sh/ruff/) | lint + format + import sort | `pyproject.toml` `[tool.ruff]` | -| [pyright](https://microsoft.github.io/pyright/) | type checker | `pyproject.toml` `[tool.pyright]` | -| [pytest](https://docs.pytest.org/) | test runner | `pyproject.toml` `[tool.pytest.ini_options]` | - -`pyright` is consumed in two places: as a dev dependency (`uv run pyright` for CI/scripted runs) and via VS Code's **Pylance** extension (which embeds pyright). The standalone `ms-pyright.pyright` extension is in `unwantedRecommendations` because Pylance covers it. `mypy` is **not used** here - don't introduce it. - -### Local Development Loop - -From inside the Python project directory: - -```sh -uv sync # creates .venv, installs deps + dev group -uv run ruff format # auto-format -uv run ruff check --fix # auto-fix lint -uv run ruff check # verify lint clean -uv run ruff format --check # verify format clean -uv run pyright # verify types -uv run pytest # run tests -uv build # produce wheel + sdist in ./dist -``` - -The Python clean-compile (see [Clean-Compile Verification](#clean-compile-verification)) is `uv run ruff format` + `uv run ruff check` + `uv run pyright`; run it (plus `uv run pytest`) before committing. These are documented commands, not VS Code tasks. CI runs the same clean-compile commands as the authoritative backstop. Git hooks are opt-in; wire `pre-commit` for `ruff` and `pyright` yourself if you want local enforcement. - -### Layout - -`src` layout - keeps the package out of the repo root and prevents accidental imports of unbuilt code: - -```text -<python-project>/ - pyproject.toml - README.md - uv.lock # committed for reproducible CI - src/ - <package_name>/ - __init__.py - _version.py - <modules>.py - tests/ - __init__.py - test_<module>.py -``` - -### Code Style - -#### Formatting and Linting - -- **`ruff format` is authoritative.** Don't argue with the formatter; if it reformats your code, that's the final form. Configure (line length, target version) in `pyproject.toml` `[tool.ruff]`, not via inline `# fmt:` directives. -- **Run `ruff check --fix` before committing.** Most ruff lint rules have safe autofixes; let the tool handle them. The configured rule families are listed under `[tool.ruff.lint]` `select`. Add new rule families project-wide rather than scattering inline `# noqa` markers. -- **`# noqa` is a last resort.** When you must use one, scope it narrowly (`# noqa: E501`, not bare `# noqa`) and add a short comment on the same line explaining why. False-positive patterns that recur across the codebase belong in `[tool.ruff.lint]` `ignore` or per-file `[tool.ruff.lint.per-file-ignores]`, with a comment. Porting an existing codebase is not a license to add `ignore` / `per-file-ignores` blocks to mute newly surfaced lint - fix it (see [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions)). - -#### Comments - -- **Inline `#` comments**: keep tight and local. One line is preferred, but multi-line is fine when you need to document a non-obvious implementation constraint, a local trade-off, or coupling that future edits could easily break. Keep that rationale next to the affected block so the reviewer/maintainer sees it at edit-time. -- **Don't explain *what* the code does** - well-named identifiers handle that. Don't reference the current task ("added for X", "used by Y"); that belongs in the PR description. - -#### Docstrings - -- Follow [PEP 257](https://peps.python.org/pep-0257/). Focus docstrings primarily on the **behavior contract** (what callers and tests can rely on), public semantics, and edge-case expectations. Implementation-local rationale belongs in inline `#` comments, not docstrings. -- A short one-liner is fine for trivial functions and tests with self-documenting names. -- For non-trivial behavior - non-obvious test scenarios, contracts a test pins, edge cases callers must know about, design trade-offs that are load-bearing for future maintainers - write a one-line summary, blank line, then a details paragraph. Multi-paragraph docstrings are fine when the contract earns it. -- Design notes belong **in the code** (docstrings or inline comments). They do NOT belong in [`HISTORY.md`](./HISTORY.md) - that file is end-user release notes, not a design log. - -#### Type Hints - -- **All public APIs are typed.** Pyright runs on `src/` in strict mode (`[tool.pyright]` `strict = ["src"]`); tests run in standard mode. -- **Use modern syntax**: `list[int]` not `List[int]`, `dict[str, X]` not `Dict[str, X]`, `X | None` not `Optional[X]`, `from __future__ import annotations` only when needed for forward references. -- **Don't add `# type: ignore` to silence pyright errors without a comment** explaining the constraint. If a recurring false positive needs suppression, configure it project-wide in `[tool.pyright]`. A new port doesn't change this - fix freshly surfaced type errors rather than muting them (see [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions)). - -#### Naming - -- `snake_case` for functions, methods, variables, modules, package directories. -- `PascalCase` for classes, type aliases, type vars, enum members. -- `UPPER_SNAKE_CASE` for module-level constants. -- Single leading underscore for module-private; double leading underscore for name-mangled (rare - usually means rethink the design). - -#### Imports - -- **Let ruff sort imports.** `[tool.ruff.lint]` `select` includes the `I` rule family (isort-equivalent). Don't hand-sort. -- Standard library first, then third-party, then first-party (the project itself), each block separated by a blank line - ruff enforces this automatically. -- Avoid wildcard imports (`from x import *`) outside `__init__.py` re-exports. - -#### Patterns to Avoid - -- **Don't add backward-compat shims, `# removed` markers, or rename-to-`_` for unused vars** - just delete. Git history is the audit trail. -- **Don't add error handling for impossible cases.** Trust internal code; only validate at boundaries (user input, parsed config, external APIs). -- **Don't use exceptions for expected control flow.** Exceptions are for *unexpected* states. -- **Don't suppress errors silently** (`except Exception: pass`). Either handle the specific exception and document why it's safe, or let it propagate. - -### Tests - -- `pytest` with the configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. -- One test file per module under test, named `test_<module>.py`. -- Test functions named `test_<scenario>_<expected_behavior>` - descriptive, not numbered. -- Use fixtures (defined in `conftest.py` for shared ones, or per-test for narrowly-scoped) instead of setup/teardown methods. -- **Avoid mocking when fakes work.** Hand-rolled fakes that implement the protocol you depend on are usually clearer and break less than `unittest.mock` magic. -- **Test edge cases that the docstring promises**, not implementation details. If the test breaks when you refactor *without changing behavior*, the test is asserting on an implementation detail. - -### Versioning - -`_version.py` ships with `__version__ = "0.0.0"` as a placeholder. Until you wire `_version.py` to something that increments (the usual options are `hatch-vcs`, a version.json bridge, or manual bumps), no new PyPI versions will land - publishing with `skip-existing: true` keeps a stuck placeholder version from failing the run. - -### Linter Cleanliness - -Before pushing or opening a PR: - -- VS Code's **Problems** pane should be quiet for the files you touched. The relevant linters are ruff (via the `charliermarsh.ruff` extension) and pyright (via the `ms-python.python` extension's bundled Pylance). -- The CI gate is `uv run ruff check && uv run ruff format --check && uv run pyright && uv run pytest` - same as the local commands above, run from the Python project directory. -- Markdown in this directory follows the repo-wide [Markdown and Spelling](#markdown-and-spelling) rules. +# Code Style and Formatting Rules + +This is the single code-style guide for the repo. The **General** section applies to every language. Each **language section** (.NET, Python) is self-contained: a repo reads only the section(s) for the languages it ships and ignores the rest. The whole file is carried, not trimmed - an unused-language section costs nothing and keeps re-sync a clean overwrite, the same carry-whole model as [`.editorconfig`](./.editorconfig), whose inert `[*.cs]` block a non-.NET repo keeps. + +Cross-cutting *process* rules (PR titles, branching, US English, markdown style, comments philosophy, workflow YAML, PR review etiquette) live in [GOVERNANCE.md](./GOVERNANCE.md) and are not repeated here. + +## General + +These rules apply to every language in the repo. + +### Tooling Names and Casing + +Use each tool's official casing in task labels, docs, and prose - `.NET` (not `.Net`), `CSharpier`, `ruff`, `pyright`, `uv`. Don't invent personal variants. + +### Clean-Compile Verification + +Each language defines a **clean-compile** verification - the combination of build, formatter, linter, and code-analysis tools that must report clean before a commit. It is exposed as one or more **named** VS Code tasks (or, where a language ships no tasks, documented commands), and those definitions are **carried verbatim** across derived repos. The concrete names live in each language section below. + +- **Run it after every code change.** The relevant language's clean-compile must pass before you commit; CI runs the same checks as a backstop. +- **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly; either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). +- **A local commit/pre-commit gate is the repo's choice.** No single hook runner fits every language (a `dotnet`-tool runner like Husky.Net suits .NET but not Python), so none is mandated - but that is **not** a recommendation against commit gates. CI is the authoritative backstop regardless; a local gate is an additive convenience a repo may wire and keep - Husky.Net (and `dotnet husky run` as a style step) for .NET, `pre-commit` for Python. Keeping a working gate is not drift. + +### Analyzer Diagnostics and Suppressions + +- **A new port is not a license to silence diagnostics.** Brownfield / just-ported status never justifies relaxing analyzer or linter severities or muting newly surfaced warnings - fix them. (The only brownfield allowance in this template is the one-time git-signing / line-ending migration described in [GOVERNANCE.md](./GOVERNANCE.md) and [README.md](./README.md), which has nothing to do with code analysis.) +- **Suppress only genuine false-positives or deliberate, documented exceptions**, always at the **narrowest scope that fits**, in this order of preference: + 1. An **in-code annotation on the specific symbol**, with a justification - the language's attribute/comment form, never a blanket pragma spanning a region. + 2. The **owning project's local config** when the exception is project-wide for one project (e.g. a test project's own `.editorconfig` / `pyproject.toml`). + 3. The **root / shared config** only when the suppression is genuinely applicable to **every** project in the repo. +- **Never blanket-relax a batch of rules project-wide** to get a port to build. The per-language mechanics (which attribute, which config key) are in each language section. + +### Markdown and Spelling + +These apply repo-wide, in every directory: + +1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc) at the repo root is the single source of truth - the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length, `MD033` inline HTML) are **intentional** - do not "fix" them. Fix violations at the source rather than disabling rules. +2. **Spelling**: All spelling must be clean via the CSpell VS Code integration; words must be correctly spelled in **US English** (the repo-wide convention - see [GOVERNANCE.md](./GOVERNANCE.md)). Project-specific terms go in [`cspell.json`](./cspell.json), which is the single source of truth for the word list; the workspace file carries no word list of its own. +3. **Spelling CI scope**: The enforced CI spell-check gate covers **`README.md` and `HISTORY.md` only** - these are the files every repo visitor sees, so they must be clean. It is deliberately **not** all `**/*.md`: repos carry many markdown files full of technical terms, and gating every one of them would mean endlessly padding `cspell.json` just to keep CI green. Broad, live spell-checking across any file (source, markdown, text) is the **cspell editor extension's** job, so typos still surface to whoever is editing. A repo owner **may** widen their own CI file list, but the template ships README + HISTORY as the default; keep every surface that runs cspell - the CI workflow and any local VS Code task or one-liner the repo has - on the same file list. The list is explicit (not a glob), so a repo that ships no `HISTORY.md` (e.g. one with no changelog) must drop it from all three surfaces and gate on `README.md` alone - cspell errors on a listed file that does not exist. Markdown *linting* (item 1) stays repo-wide `**/*.md` - it does not choke on technical terms. + +## .NET + +*This section applies only to the .NET side. A repo with no .NET projects still carries it (the file is carried whole) and ignores it.* + +This is the style guide for any **.NET projects** in this repo. + +### Build Requirements + +#### Zero Warnings Policy + +**CRITICAL**: All builds must complete without warnings. The project enforces this through: + +1. **The `.NET Format` clean-compile task** (see [Clean-Compile Verification](#clean-compile-verification)) + - The .NET clean-compile is the **`.NET Format`** VS Code task, which chains `CSharpier Format` -> `.NET Build` -> `dotnet format style --verify-no-changes`. These three task definitions are carried verbatim in [`.vscode/tasks.json`](./.vscode/tasks.json). + - After any code change it must pass before commit. Run the `.NET Format` task. To run it natively instead, reproduce that task chain from [`.vscode/tasks.json`](./.vscode/tasks.json) exactly - `CSharpier Format`, then `.NET Build`, then the `dotnet format style --verify-no-changes --severity=info ...` verify - without dropping or loosening any argument (tasks.json is the canonical command spec). Bare `dotnet format` alone, skipping CSharpier or the build, is not sufficient. + +2. **Analyzer configuration** + - `<EnableNETAnalyzers>true</EnableNETAnalyzers>` with `<AnalysisLevel>latest-all</AnalysisLevel>` and `<AnalysisMode>All</AnalysisMode>` (full analyzer set enabled) + - `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` - any diagnostic surfaced as a warning fails the build, so it must be fixed or deliberately suppressed, not left to accumulate (see [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions)) + +3. **CI lint backstop** + - CI runs the clean-compile checks on every PR as the authoritative backstop + - Git hooks are optional; a repo may wire a local runner (Husky.Net) for pre-commit enforcement, but CI is the gate that matters + +#### Build Tasks + +Available VS Code tasks (run them from VS Code's task runner - **Terminal -> Run Task** - or an agent's task-running tool). The three clean-compile tasks below are carried verbatim; a repo adds its own convenience tasks (tool updates, dependency upgrades, benchmarks) on top: + +- `.NET Build`: Build with diagnostic verbosity *(clean-compile)* +- `CSharpier Format`: Auto-format code with CSharpier *(clean-compile)* +- `.NET Format`: Run CSharpier and build, then verify formatting and style with `--verify-no-changes` *(clean-compile; the task to run after edits)* + +### Tooling and Editor + +#### Code Formatting and Tooling + +1. **CSharpier**: Primary code formatter + - Invoked by the `CSharpier Format` task / `dotnet csharpier format --log-level=debug .` +2. **dotnet format**: Style verification + - Verify no changes: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed` +3. **Other tools** + - `dotnet-outdated-tool`: Dependency update checks + - Nerdbank.GitVersioning: Version management + +CI is the authoritative lint backstop. Local pre-commit hooks are optional - wire Husky.Net (or another runner) if you want local enforcement. + +#### Editor Baseline + +1. **Required VS Code extensions**: CSharpier, markdownlint, CSpell +2. **VS Code settings**: Use the workspace settings without overrides + +### Coding Standards and Conventions + +Note: Code snippets are illustrative examples only. Replace namespaces/types to match your project. + +#### C# Language Features + +1. **File-scoped namespaces** + + ```csharp + namespace Example.Project.Library; + ``` + +2. **Nullable reference types**: Enabled (`<Nullable>enable</Nullable>`) + - Use nullable annotations appropriately + - Use `required` for mandatory properties + +3. **Modern C# features**: Prefer modern language constructs + - Primary constructors when appropriate + - Top-level statements for console apps + - Pattern matching over traditional checks + - Collection expressions when types loosely match + - Extension methods - the classic `this`-parameter form, or an `extension(<receiver>) { ... }` block on C# 14+ + - Implicit object creation when type is apparent + - Range and index operators + +4. **Expression-bodied members**: Use for applicable members + - Methods, properties, accessors, operators, lambdas, local functions + +5. **`var` keyword**: Do NOT use `var` (always use explicit types) + + ```csharp + // Correct + int count = 42; + string name = "test"; + + // Incorrect + var count = 42; + var name = "test"; + ``` + +#### Naming Conventions + +1. **Private fields**: underscore prefix with camelCase + + ```csharp + private readonly HttpClient _httpClient; + private int _counter; + ``` + +2. **Static fields**: `s_` prefix with camelCase + + ```csharp + private static int s_instanceCount; + ``` + +3. **Constants**: PascalCase + + ```csharp + private const int MaxRetries = 3; + ``` + +#### Code Structure + +1. **Global usings**: Use `GlobalUsings.cs` for common namespaces + + ```csharp + global using System; + global using System.Net.Http; + global using System.Threading.Tasks; + global using Serilog; + ``` + +2. **Usings placement**: Outside namespace, sorted with `System` directives first + + ```csharp + using System.CommandLine; + using System.Runtime.CompilerServices; + using Example.Project.Library; + + namespace Example.Project.Console; + ``` + +3. **Braces**: Allman style + + ```csharp + public void Method() + { + if (condition) + { + // code + } + } + ``` + +4. **Indentation** + - C# files: 4 spaces + - XML/csproj files: 2 spaces + - YAML files: 2 spaces + - JSON files: 4 spaces + +5. **Line endings** + - Everything: LF, per `.editorconfig`'s `[*]` default + - Windows batch and command scripts (`.bat`, `.cmd`): CRLF, the one exception + +6. **Encoding**: UTF-8 without a BOM, per `.editorconfig`'s `charset = utf-8`. + +7. **`#region`**: Do not use regions. Prefer logical file/folder/namespace organization. +8. **Member ordering (StyleCop SA1201)**: const -> static readonly -> static fields -> instance readonly fields -> instance fields -> constructors -> public (events -> properties -> indexers -> methods -> operators) -> non-public in same order -> nested types + +#### Comments and Documentation + +1. **XML documentation** + - `<GenerateDocumentationFile>true</GenerateDocumentationFile>` + - Missing XML comments for public APIs are suppressed (`.editorconfig`) + - Must document all public surfaces. + - Single-line summaries, additional details in remarks, document input parameters, return values, exceptions, and add crefs + + ```csharp + /// <summary> + /// Example of a single line summary. + /// </summary> + /// <remarks> + /// Additional important details about usage. + /// Multiple lines if needed. + /// </remarks> + /// <param name="category"> + /// The quote category to request + /// </param> + /// <param name="cancellationToken"> + /// A <see cref="System.Threading.CancellationToken"/> that can be used to cancel the request. + /// </param> + /// <returns> + /// A <see cref="string"/> containing the quote text. + /// </returns> + /// <exception cref="System.ArgumentException"> + /// Thrown when <paramref name="category"/> is not a supported value. + /// </exception> + public async Task<string> GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) {} + ``` + +2. **Leave human-authored comments exactly as written**: do not reword, trim, reflow, or "clean" a comment a person wrote, even where it bends a rule above. Revise only agent-authored comments, and match the surrounding voice when you do. The comment-growth rule that says to rewrite a comment fresh rather than bolt onto it governs agent-authored comments; it does not license collapsing a maintainer's. + +#### Analyzer Suppressions (.NET) + +Follow the scope hierarchy in [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions). .NET mechanics, narrowest first: + +- **Never use `#pragma warning disable`** to silence an analyzer. +- **Symbol-scoped**: a `[System.Diagnostics.CodeAnalysis.SuppressMessage(...)]` attribute with a `Justification`, on the specific member or type: + + ```csharp + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "https://github.com/dotnet/sdk/issues/51681" + )] + ``` + +- **Project-scoped** (e.g. a test project): a `dotnet_diagnostic.<RULE>.severity` entry in *that project's own* `.editorconfig`, with a comment explaining why. +- **Repo-wide**: a `dotnet_diagnostic.<RULE>.severity` entry in the root `.editorconfig`, only when the rule is genuinely not applicable to any project. Relaxing a batch of `CA*` rules (or `dotnet_analyzer_diagnostic.severity`) to push a brownfield port through the build is exactly what this forbids. + +#### Error Handling and Logging + +1. **Serilog logging**: Use structured logging + + ```csharp + logger.Error(exception, "{Function}", function); + ``` + +2. **Library log configuration**: Libraries must expose logging configuration + - Provide options or settings to supply an `ILoggerFactory` and/or `ILogger` + - Offer a global fallback logger for static usage when needed + +3. **CallerMemberName**: Use for automatic function name tracking + + ```csharp + public bool LogAndPropagate( + Exception exception, + [CallerMemberName] string function = "unknown" + ) + ``` + +4. **Logger extensions**: Use `Extensions.cs` for logger and other extension methods + + ```csharp + extension(ILogger logger) + { + public bool LogAndPropagate(Exception exception, ...) { } + } + ``` + +5. **Exceptions**: Do not swallow exceptions; log and rethrow or translate to a domain-specific exception + +#### Code Patterns + +1. **Guard clauses**: Prefer early returns for validation and error handling +2. **Async all the way**: Avoid blocking calls (`.Result`, `.Wait()`); use `async`/`await` +3. **Cancellation tokens**: Accept `CancellationToken` as the last parameter and pass it through +4. **ConfigureAwait**: In library code, use `ConfigureAwait(false)` unless context is required + - Do not call `ConfigureAwait(false)` in xUnit tests (see xUnit1030) +5. **Disposables**: Use `await using` for async disposables; prefer `using` declarations +6. **LINQ vs loops**: Use LINQ for clarity, loops for hot paths or allocations +7. **HTTP**: Reuse `HttpClient` via factory; avoid per-request instantiation +8. **Collections**: Prefer `IReadOnlyList<T>`/`IReadOnlyCollection<T>` for public APIs +9. **Immutability**: Prefer immutable records; use init-only setters when records are not suitable; prefer immutable or frozen collections for read-only data +10. **Exceptions as control flow**: Avoid using exceptions for expected flow +11. **Sealing classes**: Seal classes that are not designed for inheritance +12. **Read-only data**: Use immutable or frozen collections for read-only data sets +13. **Lazy initialization**: Use `Lazy<T>` for static, thread-safe instantiation (e.g., logger factory, HTTP factory) + +#### Testing Conventions + +1. **Framework**: xUnit with AwesomeAssertions + + ```csharp + [Fact] + public void MethodName_Scenario_ExpectedBehavior() + { + // Arrange + int expected = 42; + + // Act + int actual = GetValue(); + + // Assert + actual.Should().Be(expected); + } + ``` + +2. **Organization**: Arrange-Act-Assert pattern +3. **Naming**: Descriptive names with underscores +4. **Theory tests**: Use `[Theory]` with `[InlineData]` + +### Project Configuration + +1. **Target framework**: .NET 10.0 (`<TargetFramework>net10.0</TargetFramework>`) + +2. **AOT compatibility** + - `<IsAotCompatible>true</IsAotCompatible>` + - `<VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility>` + +3. **Assembly information** + - Use semantic versioning + - Include SourceLink: `<PublishRepositoryUrl>true</PublishRepositoryUrl>` + - Embed untracked sources: `<EmbedUntrackedSources>true</EmbedUntrackedSources>` + +4. **Internal visibility**: Use `InternalsVisibleTo` for test and benchmark access (adapt the project names to your repo's test/benchmark projects) + + ```xml + <ItemGroup> + <InternalsVisibleTo Include="YourBenchmarkProject" /> + <InternalsVisibleTo Include="YourTestProject" /> + </ItemGroup> + ``` + +### Best Practices + +1. **Code reviews**: All changes go through pull requests + +## Python + +*This section applies only to the Python side. A repo with no Python projects still carries it (the file is carried whole) and ignores it.* + +This is the style guide for any **Python project(s)** in this repo. + +### Toolchain + +| Tool | Role | Config | +|---|---|---| +| [uv](https://docs.astral.sh/uv/) | env, deps, build, publish | `pyproject.toml` `[dependency-groups]`, `uv.lock` | +| [hatchling](https://hatch.pypa.io/latest/) | build backend | `pyproject.toml` `[build-system]` | +| [ruff](https://docs.astral.sh/ruff/) | lint + format + import sort | `pyproject.toml` `[tool.ruff]` | +| [pyright](https://microsoft.github.io/pyright/) | type checker | `pyproject.toml` `[tool.pyright]` | +| [pytest](https://docs.pytest.org/) | test runner | `pyproject.toml` `[tool.pytest.ini_options]` | + +`pyright` is consumed in two places: as a dev dependency (`uv run pyright` for CI/scripted runs) and via VS Code's **Pylance** extension (which embeds pyright). The standalone `ms-pyright.pyright` extension is in `unwantedRecommendations` because Pylance covers it. `mypy` is **not used** here - don't introduce it. + +### Local Development Loop + +From inside the Python project directory: + +```sh +uv sync # creates .venv, installs deps + dev group +uv run ruff format # auto-format +uv run ruff check --fix # auto-fix lint +uv run ruff check # verify lint clean +uv run ruff format --check # verify format clean +uv run pyright # verify types +uv run pytest # run tests +uv build # produce wheel + sdist in ./dist +``` + +The Python clean-compile (see [Clean-Compile Verification](#clean-compile-verification)) is `uv run ruff format` + `uv run ruff check` + `uv run pyright`; run it (plus `uv run pytest`) before committing. These are documented commands, not VS Code tasks. CI runs the same clean-compile commands as the authoritative backstop. Git hooks are opt-in; wire `pre-commit` for `ruff` and `pyright` yourself if you want local enforcement. + +### Layout + +`src` layout - keeps the package out of the repo root and prevents accidental imports of unbuilt code: + +```text +<python-project>/ + pyproject.toml + README.md + uv.lock # committed for reproducible CI + src/ + <package_name>/ + __init__.py + _version.py + <modules>.py + tests/ + __init__.py + test_<module>.py +``` + +### Code Style + +#### Formatting and Linting + +- **`ruff format` is authoritative.** Don't argue with the formatter; if it reformats your code, that's the final form. Configure (line length, target version) in `pyproject.toml` `[tool.ruff]`, not via inline `# fmt:` directives. +- **Run `ruff check --fix` before committing.** Most ruff lint rules have safe autofixes; let the tool handle them. The configured rule families are listed under `[tool.ruff.lint]` `select`. Add new rule families project-wide rather than scattering inline `# noqa` markers. +- **`# noqa` is a last resort.** When you must use one, scope it narrowly (`# noqa: E501`, not bare `# noqa`) and add a short comment on the same line explaining why. False-positive patterns that recur across the codebase belong in `[tool.ruff.lint]` `ignore` or per-file `[tool.ruff.lint.per-file-ignores]`, with a comment. Porting an existing codebase is not a license to add `ignore` / `per-file-ignores` blocks to mute newly surfaced lint - fix it (see [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions)). + +#### Comments + +- **Inline `#` comments**: keep tight and local. One line is preferred, but multi-line is fine when you need to document a non-obvious implementation constraint, a local trade-off, or coupling that future edits could easily break. Keep that rationale next to the affected block so the reviewer/maintainer sees it at edit-time. +- **Don't explain *what* the code does** - well-named identifiers handle that. Don't reference the current task ("added for X", "used by Y"); that belongs in the PR description. + +#### Docstrings + +- Follow [PEP 257](https://peps.python.org/pep-0257/). Focus docstrings primarily on the **behavior contract** (what callers and tests can rely on), public semantics, and edge-case expectations. Implementation-local rationale belongs in inline `#` comments, not docstrings. +- A short one-liner is fine for trivial functions and tests with self-documenting names. +- For non-trivial behavior - non-obvious test scenarios, contracts a test pins, edge cases callers must know about, design trade-offs that are load-bearing for future maintainers - write a one-line summary, blank line, then a details paragraph. Multi-paragraph docstrings are fine when the contract earns it. +- Design notes belong **in the code** (docstrings or inline comments). They do NOT belong in [`HISTORY.md`](./HISTORY.md) - that file is end-user release notes, not a design log. + +#### Type Hints + +- **All public APIs are typed.** Pyright runs on `src/` in strict mode (`[tool.pyright]` `strict = ["src"]`); tests run in standard mode. +- **Use modern syntax**: `list[int]` not `List[int]`, `dict[str, X]` not `Dict[str, X]`, `X | None` not `Optional[X]`, `from __future__ import annotations` only when needed for forward references. +- **Don't add `# type: ignore` to silence pyright errors without a comment** explaining the constraint. If a recurring false positive needs suppression, configure it project-wide in `[tool.pyright]`. A new port doesn't change this - fix freshly surfaced type errors rather than muting them (see [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions)). + +#### Naming + +- `snake_case` for functions, methods, variables, modules, package directories. +- `PascalCase` for classes, type aliases, type vars, enum members. +- `UPPER_SNAKE_CASE` for module-level constants. +- Single leading underscore for module-private; double leading underscore for name-mangled (rare - usually means rethink the design). + +#### Imports + +- **Let ruff sort imports.** `[tool.ruff.lint]` `select` includes the `I` rule family (isort-equivalent). Don't hand-sort. +- Standard library first, then third-party, then first-party (the project itself), each block separated by a blank line - ruff enforces this automatically. +- Avoid wildcard imports (`from x import *`) outside `__init__.py` re-exports. + +#### Patterns to Avoid + +- **Don't add backward-compat shims, `# removed` markers, or rename-to-`_` for unused vars** - just delete. Git history is the audit trail. +- **Don't add error handling for impossible cases.** Trust internal code; only validate at boundaries (user input, parsed config, external APIs). +- **Don't use exceptions for expected control flow.** Exceptions are for *unexpected* states. +- **Don't suppress errors silently** (`except Exception: pass`). Either handle the specific exception and document why it's safe, or let it propagate. + +### Tests + +- `pytest` with the configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. +- One test file per module under test, named `test_<module>.py`. +- Test functions named `test_<scenario>_<expected_behavior>` - descriptive, not numbered. +- Use fixtures (defined in `conftest.py` for shared ones, or per-test for narrowly-scoped) instead of setup/teardown methods. +- **Avoid mocking when fakes work.** Hand-rolled fakes that implement the protocol you depend on are usually clearer and break less than `unittest.mock` magic. +- **Test edge cases that the docstring promises**, not implementation details. If the test breaks when you refactor *without changing behavior*, the test is asserting on an implementation detail. + +### Versioning + +`_version.py` ships with `__version__ = "0.0.0"` as a placeholder. Until you wire `_version.py` to something that increments (the usual options are `hatch-vcs`, a version.json bridge, or manual bumps), no new PyPI versions will land - publishing with `skip-existing: true` keeps a stuck placeholder version from failing the run. + +### Linter Cleanliness + +Before pushing or opening a PR: + +- VS Code's **Problems** pane should be quiet for the files you touched. The relevant linters are ruff (via the `charliermarsh.ruff` extension) and pyright (via the `ms-python.python` extension's bundled Pylance). +- The CI gate is `uv run ruff check && uv run ruff format --check && uv run pyright && uv run pytest` - same as the local commands above, run from the Python project directory. +- Markdown in this directory follows the repo-wide [Markdown and Spelling](#markdown-and-spelling) rules. diff --git a/CreateMatrix/.editorconfig b/CreateMatrix/.editorconfig index 75e848c1..08f2b3f8 100644 --- a/CreateMatrix/.editorconfig +++ b/CreateMatrix/.editorconfig @@ -1,10 +1,10 @@ -root = false - -# C# files -[*.cs] - -# Ignore types can be made internal -dotnet_diagnostic.CA1515.severity = none - -# Ignore casing rule for invariant culture -dotnet_diagnostic.CA1308.severity = none +root = false + +# C# files +[*.cs] + +# Ignore types can be made internal +dotnet_diagnostic.CA1515.severity = none + +# Ignore casing rule for invariant culture +dotnet_diagnostic.CA1308.severity = none diff --git a/CreateMatrix/AssemblyInfo.cs b/CreateMatrix/AssemblyInfo.cs index d5a92a5a..569bcdc2 100644 --- a/CreateMatrix/AssemblyInfo.cs +++ b/CreateMatrix/AssemblyInfo.cs @@ -1,45 +1,45 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -namespace CreateMatrix; - -internal static class AssemblyInfo -{ - internal static string AppVersion => $"{AppName} : {FileVersion} ({BuildType})"; - - internal static string RuntimeVersion => - $"{RuntimeInformation.FrameworkDescription} : {RuntimeInformation.RuntimeIdentifier}"; - - internal static string BuildType => -#if DEBUG - "Debug"; -#else - "Release"; -#endif - - internal static string AppName => GetAssembly().GetName().Name ?? string.Empty; - - internal static string InformationalVersion => - // E.g. 1.2.3+abc123.abc123 - GetAssembly() - .GetCustomAttribute<AssemblyInformationalVersionAttribute>() - ?.InformationalVersion - ?? string.Empty; - - internal static string FileVersion => - // E.g. 1.2.3.4 - GetAssembly().GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version - ?? string.Empty; - - internal static string ReleaseVersion => - // E.g. 1.2.3 part of 1.2.3+abc123.abc123 - // Use major.minor.build from informational version - InformationalVersion.Split('+', '-')[0]; - - private static Assembly GetAssembly() - { - Assembly? assembly = Assembly.GetEntryAssembly(); - assembly ??= Assembly.GetExecutingAssembly(); - return assembly; - } -} +using System.Reflection; +using System.Runtime.InteropServices; + +namespace CreateMatrix; + +internal static class AssemblyInfo +{ + internal static string AppVersion => $"{AppName} : {FileVersion} ({BuildType})"; + + internal static string RuntimeVersion => + $"{RuntimeInformation.FrameworkDescription} : {RuntimeInformation.RuntimeIdentifier}"; + + internal static string BuildType => +#if DEBUG + "Debug"; +#else + "Release"; +#endif + + internal static string AppName => GetAssembly().GetName().Name ?? string.Empty; + + internal static string InformationalVersion => + // E.g. 1.2.3+abc123.abc123 + GetAssembly() + .GetCustomAttribute<AssemblyInformationalVersionAttribute>() + ?.InformationalVersion + ?? string.Empty; + + internal static string FileVersion => + // E.g. 1.2.3.4 + GetAssembly().GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version + ?? string.Empty; + + internal static string ReleaseVersion => + // E.g. 1.2.3 part of 1.2.3+abc123.abc123 + // Use major.minor.build from informational version + InformationalVersion.Split('+', '-')[0]; + + private static Assembly GetAssembly() + { + Assembly? assembly = Assembly.GetEntryAssembly(); + assembly ??= Assembly.GetExecutingAssembly(); + return assembly; + } +} diff --git a/CreateMatrix/CommandLine.cs b/CreateMatrix/CommandLine.cs index adbbf7d0..6be1dcde 100644 --- a/CreateMatrix/CommandLine.cs +++ b/CreateMatrix/CommandLine.cs @@ -1,151 +1,151 @@ -using System.Collections.Frozen; -using System.CommandLine; -using System.CommandLine.Parsing; - -namespace CreateMatrix; - -internal sealed class CommandLine -{ - private static readonly Option<FileInfo> s_versionPathOption = new Option<FileInfo>( - "--versionpath" - ) - { - Description = "Version JSON file path.", - }.AcceptLegalFilePathsOnly(); - - private static readonly Option<FileInfo> s_matrixPathOption = new Option<FileInfo>( - "--matrixpath" - ) - { - Description = "Matrix JSON file path.", - }.AcceptLegalFilePathsOnly(); - - private static readonly Option<bool> s_updateVersionOption = new("--updateversion") - { - Description = "Update version information from online sources.", - DefaultValueFactory = _ => false, - }; - - private static readonly Option<DirectoryInfo> s_makeDirectoryOption = new Option<DirectoryInfo>( - "--makedirectory" - ) - { - Description = "Make directory path.", - }.AcceptExistingOnly(); - - private static readonly Option<DirectoryInfo> s_dockerDirectoryOption = - new Option<DirectoryInfo>("--dockerdirectory") - { - Description = "Docker directory path.", - }.AcceptExistingOnly(); - - private static readonly Option<VersionInfo.LabelType> s_versionLabelOption = new( - "--versionlabel" - ) - { - Description = "Version label to apply.", - DefaultValueFactory = _ => VersionInfo.LabelType.Latest, - }; - - internal CommandLine(string[] args) - { - Root = CreateRootCommand(); - Result = Root.Parse(args, new ParserConfiguration { EnablePosixBundling = false }); - Result.InvocationConfiguration.EnableDefaultExceptionHandler = false; - } - - private static readonly FrozenSet<string> s_cliBypassList = FrozenSet.Create( - StringComparer.OrdinalIgnoreCase, - "--help", - "--version" - ); - - internal RootCommand Root { get; } - internal ParseResult Result { get; } - - internal static RootCommand CreateRootCommand() - { - RootCommand rootCommand = new( - "CreateMatrix utility to create a matrix of builds from online product versions" - ); - rootCommand.Subcommands.Add(CreateVersionCommand()); - rootCommand.Subcommands.Add(CreateMatrixCommand()); - rootCommand.Subcommands.Add(CreateMakeCommand()); - return rootCommand; - } - - private static Command CreateVersionCommand() - { - Command versionCommand = new( - "version", - "Create version information file from online sources" - ) - { - s_versionPathOption, - }; - versionCommand.SetAction( - (parseResult, cancellationToken) => - new Program(CreateOptions(parseResult), cancellationToken).ExecuteVersionAsync() - ); - return versionCommand; - } - - private static Command CreateMatrixCommand() - { - Command matrixCommand = new("matrix", "Create matrix information file from online sources") - { - s_versionPathOption.AcceptExistingOnly(), - s_matrixPathOption, - s_updateVersionOption, - }; - matrixCommand.SetAction( - (parseResult, cancellationToken) => - new Program(CreateOptions(parseResult), cancellationToken).ExecuteMatrixAsync() - ); - return matrixCommand; - } - - private static Command CreateMakeCommand() - { - Command makeCommand = new("make", "Create Docker and Compose files from version file") - { - s_versionPathOption.AcceptExistingOnly(), - s_makeDirectoryOption, - s_dockerDirectoryOption, - s_versionLabelOption, - }; - makeCommand.SetAction( - (parseResult, cancellationToken) => - new Program(CreateOptions(parseResult), cancellationToken).ExecuteMakeAsync() - ); - return makeCommand; - } - - internal static Options CreateOptions(ParseResult parseResult) => - new() - { - VersionPath = parseResult.GetValue(s_versionPathOption), - MatrixPath = parseResult.GetValue(s_matrixPathOption), - UpdateVersion = parseResult.GetValue(s_updateVersionOption), - MakeDirectory = parseResult.GetValue(s_makeDirectoryOption), - DockerDirectory = parseResult.GetValue(s_dockerDirectoryOption), - VersionLabel = parseResult.GetValue(s_versionLabelOption), - }; - - internal static bool BypassStartup(ParseResult parseResult) => - parseResult.Errors.Count > 0 - || parseResult.CommandResult.Children.Any(symbolResult => - symbolResult is OptionResult optionResult - && s_cliBypassList.Contains(optionResult.Option.Name) - ); - - internal sealed class Options - { - internal required FileInfo? VersionPath { get; init; } - internal required FileInfo? MatrixPath { get; init; } - internal required bool UpdateVersion { get; init; } - internal required DirectoryInfo? MakeDirectory { get; init; } - internal required DirectoryInfo? DockerDirectory { get; init; } - internal required VersionInfo.LabelType VersionLabel { get; init; } - } -} +using System.Collections.Frozen; +using System.CommandLine; +using System.CommandLine.Parsing; + +namespace CreateMatrix; + +internal sealed class CommandLine +{ + private static readonly Option<FileInfo> s_versionPathOption = new Option<FileInfo>( + "--versionpath" + ) + { + Description = "Version JSON file path.", + }.AcceptLegalFilePathsOnly(); + + private static readonly Option<FileInfo> s_matrixPathOption = new Option<FileInfo>( + "--matrixpath" + ) + { + Description = "Matrix JSON file path.", + }.AcceptLegalFilePathsOnly(); + + private static readonly Option<bool> s_updateVersionOption = new("--updateversion") + { + Description = "Update version information from online sources.", + DefaultValueFactory = _ => false, + }; + + private static readonly Option<DirectoryInfo> s_makeDirectoryOption = new Option<DirectoryInfo>( + "--makedirectory" + ) + { + Description = "Make directory path.", + }.AcceptExistingOnly(); + + private static readonly Option<DirectoryInfo> s_dockerDirectoryOption = + new Option<DirectoryInfo>("--dockerdirectory") + { + Description = "Docker directory path.", + }.AcceptExistingOnly(); + + private static readonly Option<VersionInfo.LabelType> s_versionLabelOption = new( + "--versionlabel" + ) + { + Description = "Version label to apply.", + DefaultValueFactory = _ => VersionInfo.LabelType.Latest, + }; + + internal CommandLine(string[] args) + { + Root = CreateRootCommand(); + Result = Root.Parse(args, new ParserConfiguration { EnablePosixBundling = false }); + Result.InvocationConfiguration.EnableDefaultExceptionHandler = false; + } + + private static readonly FrozenSet<string> s_cliBypassList = FrozenSet.Create( + StringComparer.OrdinalIgnoreCase, + "--help", + "--version" + ); + + internal RootCommand Root { get; } + internal ParseResult Result { get; } + + internal static RootCommand CreateRootCommand() + { + RootCommand rootCommand = new( + "CreateMatrix utility to create a matrix of builds from online product versions" + ); + rootCommand.Subcommands.Add(CreateVersionCommand()); + rootCommand.Subcommands.Add(CreateMatrixCommand()); + rootCommand.Subcommands.Add(CreateMakeCommand()); + return rootCommand; + } + + private static Command CreateVersionCommand() + { + Command versionCommand = new( + "version", + "Create version information file from online sources" + ) + { + s_versionPathOption, + }; + versionCommand.SetAction( + (parseResult, cancellationToken) => + new Program(CreateOptions(parseResult), cancellationToken).ExecuteVersionAsync() + ); + return versionCommand; + } + + private static Command CreateMatrixCommand() + { + Command matrixCommand = new("matrix", "Create matrix information file from online sources") + { + s_versionPathOption.AcceptExistingOnly(), + s_matrixPathOption, + s_updateVersionOption, + }; + matrixCommand.SetAction( + (parseResult, cancellationToken) => + new Program(CreateOptions(parseResult), cancellationToken).ExecuteMatrixAsync() + ); + return matrixCommand; + } + + private static Command CreateMakeCommand() + { + Command makeCommand = new("make", "Create Docker and Compose files from version file") + { + s_versionPathOption.AcceptExistingOnly(), + s_makeDirectoryOption, + s_dockerDirectoryOption, + s_versionLabelOption, + }; + makeCommand.SetAction( + (parseResult, cancellationToken) => + new Program(CreateOptions(parseResult), cancellationToken).ExecuteMakeAsync() + ); + return makeCommand; + } + + internal static Options CreateOptions(ParseResult parseResult) => + new() + { + VersionPath = parseResult.GetValue(s_versionPathOption), + MatrixPath = parseResult.GetValue(s_matrixPathOption), + UpdateVersion = parseResult.GetValue(s_updateVersionOption), + MakeDirectory = parseResult.GetValue(s_makeDirectoryOption), + DockerDirectory = parseResult.GetValue(s_dockerDirectoryOption), + VersionLabel = parseResult.GetValue(s_versionLabelOption), + }; + + internal static bool BypassStartup(ParseResult parseResult) => + parseResult.Errors.Count > 0 + || parseResult.CommandResult.Children.Any(symbolResult => + symbolResult is OptionResult optionResult + && s_cliBypassList.Contains(optionResult.Option.Name) + ); + + internal sealed class Options + { + internal required FileInfo? VersionPath { get; init; } + internal required FileInfo? MatrixPath { get; init; } + internal required bool UpdateVersion { get; init; } + internal required DirectoryInfo? MakeDirectory { get; init; } + internal required DirectoryInfo? DockerDirectory { get; init; } + internal required VersionInfo.LabelType VersionLabel { get; init; } + } +} diff --git a/CreateMatrix/ComposeFile.cs b/CreateMatrix/ComposeFile.cs index 8845c396..3c1a0323 100644 --- a/CreateMatrix/ComposeFile.cs +++ b/CreateMatrix/ComposeFile.cs @@ -1,211 +1,211 @@ -namespace CreateMatrix; - -internal static class ComposeFile -{ - public static void Create(DirectoryInfo makeDirectory) - { - string makePath = makeDirectory.FullName; - - // Create local Compose file - string composeFile = CreateComposefile(null); - string filePath = Path.Combine(makePath, "Test.yml"); - Log.Logger.Information("Writing Compose file to {Path}", filePath); - File.WriteAllText(filePath, composeFile); - - // Create develop Compose file - composeFile = CreateComposefile("develop"); - filePath = Path.Combine(makePath, "Test-develop.yml"); - Log.Logger.Information("Writing Compose file to {Path}", filePath); - File.WriteAllText(filePath, composeFile); - - // Create latest Compose file - composeFile = CreateComposefile("latest"); - filePath = Path.Combine(makePath, "Test-latest.yml"); - Log.Logger.Information("Writing Compose file to {Path}", filePath); - File.WriteAllText(filePath, composeFile); - } - - private static string CreateComposefile(string? label) - { - // TODO: Switch to volume sub-paths on Moby v26+ - // https://github.com/moby/moby/pull/45687 - - // Compose file header - StringBuilder stringBuilder = new(); - _ = stringBuilder.AppendLineCrlf( - """ - # Compose file created by CreateMatrix, do not modify by hand - - """ - ); - - // Create volumes - _ = stringBuilder.AppendLineCrlf(CreateVolumes()); - // Create services - _ = stringBuilder.AppendLineCrlf(CreateServices(label)); - - return stringBuilder.ToString(); - } - - private static string CreateVolumes() - { - StringBuilder stringBuilder = new(); - _ = stringBuilder.AppendLineCrlf( - """ - volumes: - - """ - ); - - // Create a volume for every product - foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) - { - // Standard - _ = stringBuilder.AppendLineCrlf(CreateVolume(productType, false)); - - // LSIO - _ = stringBuilder.AppendLineCrlf(CreateVolume(productType, true)); - } - - return stringBuilder.ToString(); - } - - private static string CreateVolume(ProductInfo.ProductType productType, bool lsio) => - lsio - ? $$""" - # Dockerfile : {{ProductInfo.GetDocker(productType, lsio)}} - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_config: - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_media: - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_backup: - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_analytics: - - """ - : $$""" - # Dockerfile : {{ProductInfo.GetDocker(productType, lsio)}} - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_etc: - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_ini: - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_var: - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_media: - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_backup: - test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_analytics: - - """; - - private static string CreateServices(string? label) - { - StringBuilder stringBuilder = new(); - _ = stringBuilder.AppendLineCrlf( - """ - services: - - """ - ); - - // Create a service for every product - int standardPort = 7101, - lsioPort = 7201; - foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) - { - // Standard - _ = stringBuilder.AppendLineCrlf( - CreateService(productType, false, standardPort++, label) - ); - // LSIO - _ = stringBuilder.AppendLineCrlf(CreateService(productType, true, lsioPort++, label)); - } - - return stringBuilder.ToString(); - } - - private static string CreateService( - ProductInfo.ProductType productType, - bool lsio, - int port, - string? label - ) - { - string image = string.IsNullOrEmpty(label) - ? $"test_{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}" - : $"docker.io/ptr727/{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}:{label}"; - string service = $$""" - # Dockerfile : {{ProductInfo.GetDocker(productType, lsio)}} - # Port : {{port}} - {{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}: - image: {{image}} - container_name: {{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - {{port}}:7001 - - """; - - if (lsio) - { - service += $$""" - volumes: - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_config:/config - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_media:/media - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_backup:/backup - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_analytics:/analytics - - """; - } - else - { - service += $$""" - volumes: - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_etc:/opt/{{ProductInfo.GetCompany( - productType - ).ToLowerInvariant()}}/mediaserver/etc - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_ini:/home/{{ProductInfo.GetCompany( - productType - ).ToLowerInvariant()}}/.config/nx_ini - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_var:/opt/{{ProductInfo.GetCompany( - productType - ).ToLowerInvariant()}}/mediaserver/var - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_media:/media - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_backup:/backup - - test_{{ProductInfo.GetDocker( - productType, - lsio - ).ToLowerInvariant()}}_analytics:/analytics - - """; - } - - return service; - } -} +namespace CreateMatrix; + +internal static class ComposeFile +{ + public static void Create(DirectoryInfo makeDirectory) + { + string makePath = makeDirectory.FullName; + + // Create local Compose file + string composeFile = CreateComposefile(null); + string filePath = Path.Combine(makePath, "Test.yml"); + Log.Logger.Information("Writing Compose file to {Path}", filePath); + File.WriteAllText(filePath, composeFile.ReplaceLineEndings("\n").TrimEnd('\n') + "\n"); + + // Create develop Compose file + composeFile = CreateComposefile("develop"); + filePath = Path.Combine(makePath, "Test-develop.yml"); + Log.Logger.Information("Writing Compose file to {Path}", filePath); + File.WriteAllText(filePath, composeFile.ReplaceLineEndings("\n").TrimEnd('\n') + "\n"); + + // Create latest Compose file + composeFile = CreateComposefile("latest"); + filePath = Path.Combine(makePath, "Test-latest.yml"); + Log.Logger.Information("Writing Compose file to {Path}", filePath); + File.WriteAllText(filePath, composeFile.ReplaceLineEndings("\n").TrimEnd('\n') + "\n"); + } + + private static string CreateComposefile(string? label) + { + // TODO: Switch to volume sub-paths on Moby v26+ + // https://github.com/moby/moby/pull/45687 + + // Compose file header + StringBuilder stringBuilder = new(); + _ = stringBuilder.AppendLineLf( + """ + # Compose file created by CreateMatrix, do not modify by hand + + """ + ); + + // Create volumes + _ = stringBuilder.AppendLineLf(CreateVolumes()); + // Create services + _ = stringBuilder.AppendLineLf(CreateServices(label)); + + return stringBuilder.ToString(); + } + + private static string CreateVolumes() + { + StringBuilder stringBuilder = new(); + _ = stringBuilder.AppendLineLf( + """ + volumes: + + """ + ); + + // Create a volume for every product + foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) + { + // Standard + _ = stringBuilder.AppendLineLf(CreateVolume(productType, false)); + + // LSIO + _ = stringBuilder.AppendLineLf(CreateVolume(productType, true)); + } + + return stringBuilder.ToString(); + } + + private static string CreateVolume(ProductInfo.ProductType productType, bool lsio) => + lsio + ? $$""" + # Dockerfile : {{ProductInfo.GetDocker(productType, lsio)}} + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_config: + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_media: + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_backup: + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_analytics: + + """ + : $$""" + # Dockerfile : {{ProductInfo.GetDocker(productType, lsio)}} + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_etc: + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_ini: + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_var: + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_media: + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_backup: + test_{{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}_analytics: + + """; + + private static string CreateServices(string? label) + { + StringBuilder stringBuilder = new(); + _ = stringBuilder.AppendLineLf( + """ + services: + + """ + ); + + // Create a service for every product + int standardPort = 7101, + lsioPort = 7201; + foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) + { + // Standard + _ = stringBuilder.AppendLineLf( + CreateService(productType, false, standardPort++, label) + ); + // LSIO + _ = stringBuilder.AppendLineLf(CreateService(productType, true, lsioPort++, label)); + } + + return stringBuilder.ToString(); + } + + private static string CreateService( + ProductInfo.ProductType productType, + bool lsio, + int port, + string? label + ) + { + string image = string.IsNullOrEmpty(label) + ? $"test_{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}" + : $"docker.io/ptr727/{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}:{label}"; + string service = $$""" + # Dockerfile : {{ProductInfo.GetDocker(productType, lsio)}} + # Port : {{port}} + {{ProductInfo.GetDocker(productType, lsio).ToLowerInvariant()}}: + image: {{image}} + container_name: {{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - {{port}}:7001 + + """; + + if (lsio) + { + service += $$""" + volumes: + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_config:/config + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_media:/media + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_backup:/backup + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_analytics:/analytics + + """; + } + else + { + service += $$""" + volumes: + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_etc:/opt/{{ProductInfo.GetCompany( + productType + ).ToLowerInvariant()}}/mediaserver/etc + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_ini:/home/{{ProductInfo.GetCompany( + productType + ).ToLowerInvariant()}}/.config/nx_ini + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_var:/opt/{{ProductInfo.GetCompany( + productType + ).ToLowerInvariant()}}/mediaserver/var + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_media:/media + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_backup:/backup + - test_{{ProductInfo.GetDocker( + productType, + lsio + ).ToLowerInvariant()}}_analytics:/analytics + + """; + } + + return service; + } +} diff --git a/CreateMatrix/CreateMatrix.csproj b/CreateMatrix/CreateMatrix.csproj index 26aef544..47d31488 100644 --- a/CreateMatrix/CreateMatrix.csproj +++ b/CreateMatrix/CreateMatrix.csproj @@ -1,28 +1,28 @@ -<Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <AnalysisLevel>latest-all</AnalysisLevel> - <EnableNETAnalyzers>true</EnableNETAnalyzers> - <InformationalVersion>1.0.0-pre</InformationalVersion> - <IsPackable>false</IsPackable> - <Nullable>enable</Nullable> - <OutputType>Exe</OutputType> - <PublishAot>false</PublishAot> - <TargetFramework>net10.0</TargetFramework> - <Version>1.0.0</Version> - </PropertyGroup> - <PropertyGroup Condition="'$(PublishAot)' == 'true'"> - <InvariantGlobalization>true</InvariantGlobalization> - <SelfContained>true</SelfContained> - <VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.9.0" /> - <PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" /> - <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" /> - <PackageReference Include="Serilog" Version="4.4.0" /> - <PackageReference Include="System.CommandLine" Version="2.0.11" /> - </ItemGroup> - <ItemGroup> - <InternalsVisibleTo Include="CreateMatrixTests" /> - </ItemGroup> -</Project> +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <AnalysisLevel>latest-all</AnalysisLevel> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + <InformationalVersion>1.0.0-pre</InformationalVersion> + <IsPackable>false</IsPackable> + <Nullable>enable</Nullable> + <OutputType>Exe</OutputType> + <PublishAot>false</PublishAot> + <TargetFramework>net10.0</TargetFramework> + <Version>1.0.0</Version> + </PropertyGroup> + <PropertyGroup Condition="'$(PublishAot)' == 'true'"> + <InvariantGlobalization>true</InvariantGlobalization> + <SelfContained>true</SelfContained> + <VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.9.0" /> + <PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" /> + <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" /> + <PackageReference Include="Serilog" Version="4.4.0" /> + <PackageReference Include="System.CommandLine" Version="2.0.11" /> + </ItemGroup> + <ItemGroup> + <InternalsVisibleTo Include="CreateMatrixTests" /> + </ItemGroup> +</Project> diff --git a/CreateMatrix/Dockerfile.cs b/CreateMatrix/Dockerfile.cs index a3bf4578..f430ed8f 100644 --- a/CreateMatrix/Dockerfile.cs +++ b/CreateMatrix/Dockerfile.cs @@ -1,338 +1,338 @@ -namespace CreateMatrix; - -internal static class DockerFile -{ - private const string UbuntuBaseTag = "noble"; - private const string BaseImage = "docker.io/ptr727/nx-base:ubuntu-" + UbuntuBaseTag; - private const string BaseImageLsio = "docker.io/ptr727/nx-base-lsio:ubuntu-" + UbuntuBaseTag; - - public static void Create( - List<ProductInfo> productList, - string dockerPath, - VersionInfo.LabelType label - ) - { - CreateBaseDockerfiles(dockerPath); - - // Create a Docker file for each product type - foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) - { - // Find the matching product - ProductInfo productInfo = - productList.Find(item => item.Product == productType) - ?? throw new InvalidOperationException($"Product not found: {productType}"); - - // Get the version for the label, not all releases include Beta and RC labels - VersionInfo? versionInfo = productInfo.Versions.Find(item => - item.Labels.Contains(label) - ); - - // If the specific label is not found, use the latest version - if (versionInfo == null) - { - Log.Logger.Warning( - "Label {Label} not found for {Product}, using latest", - label, - productType - ); - versionInfo = productInfo.Versions.Find(item => - item.Labels.Contains(VersionInfo.LabelType.Latest) - ); - } - if (versionInfo == null) - { - throw new InvalidOperationException( - $"{productType}: No version found for label {label} or Latest" - ); - } - - // Create the standard Docker file - string dockerFile = CreateDockerfile(productType, versionInfo, false); - string filePath = Path.Combine( - dockerPath, - $"{ProductInfo.GetDocker(productType, false)}.Dockerfile" - ); - WriteDockerfile(filePath, dockerFile); - - // Create the LSIO Docker file - dockerFile = CreateDockerfile(productType, versionInfo, true); - filePath = Path.Combine( - dockerPath, - $"{ProductInfo.GetDocker(productType, true)}.Dockerfile" - ); - WriteDockerfile(filePath, dockerFile); - } - } - - private static void WriteDockerfile(string filePath, string dockerFile) - { - Log.Logger.Information("Writing Dockerfile to {Path}", filePath); - File.WriteAllText(filePath, dockerFile.ReplaceLineEndings("\n").TrimEnd('\n') + "\n"); - } - - private static string CreateDockerfile( - ProductInfo.ProductType productType, - VersionInfo versionInfo, - bool lsio - ) - { - // From - StringBuilder stringBuilder = new(); - _ = stringBuilder.AppendLineCrlf(CreateFrom(productType, lsio)); - - // Args - _ = stringBuilder.AppendLineCrlf(CreateArgs(productType, versionInfo, lsio)); - // Install - _ = stringBuilder.AppendLineCrlf(CreateInstall(lsio)); - - // Entrypoint - _ = stringBuilder.AppendLineCrlf(CreateEntrypoint(productType, lsio)); - - return stringBuilder.ToString(); - } - - private static string CreateFrom(ProductInfo.ProductType productType, bool lsio) - { - string from = $$""" - # Dockerfile created by CreateMatrix, do not modify by hand - # Product: {{productType}} - # Description: {{ProductInfo.GetDescription(productType)}} - # Company: {{ProductInfo.GetCompany(productType)}} - # Release: {{ProductInfo.GetRelease(productType)}} - # LSIO: {{lsio}} - - # https://support.networkoptix.com/hc/en-us/articles/205313168-Nx-Witness-Operating-System-Support - # Latest Ubuntu supported for v6 is Noble - # Base images are built in this repo, see Docker/NxBase*.Dockerfile - - """; - if (lsio) - { - from += $$""" - FROM {{BaseImageLsio}} - - """; - } - else - { - from += $$""" - FROM {{BaseImage}} - - """; - } - return from; - } - - private static void CreateBaseDockerfiles(string dockerPath) - { - string baseDockerfile = $$""" - # Base Dockerfile for Nx Witness images - # Built from ubuntu:{{UbuntuBaseTag}} - FROM ubuntu:{{UbuntuBaseTag}} - - # Prevent EULA and confirmation prompts in installers - ARG DEBIAN_FRONTEND=noninteractive - - # Common packages used by all product images - # https://github.com/ptr727/NxWitness/issues/282 - RUN apt-get update \ - && apt-get upgrade --yes \ - && apt-get install --no-install-recommends --yes \ - ca-certificates \ - gdb \ - libdrm2 \ - sudo \ - unzip \ - wget \ - && apt-get clean \ - && apt-get autoremove --purge --yes \ - && rm -rf /var/lib/apt/lists/* - """; - - string baseFilePath = Path.Combine(dockerPath, "NxBase.Dockerfile"); - WriteDockerfile(baseFilePath, baseDockerfile); - - string baseLsioDockerfile = $$""" - # Base Dockerfile for Nx Witness LSIO images - # Built from lsiobase/ubuntu:{{UbuntuBaseTag}} - FROM lsiobase/ubuntu:{{UbuntuBaseTag}} - - # Prevent EULA and confirmation prompts in installers - ARG DEBIAN_FRONTEND=noninteractive - - # Common packages used by all product images - # https://github.com/ptr727/NxWitness/issues/282 - RUN apt-get update \ - && apt-get upgrade --yes \ - && apt-get install --no-install-recommends --yes \ - ca-certificates \ - gdb \ - libdrm2 \ - unzip \ - wget \ - && apt-get clean \ - && apt-get autoremove --purge --yes \ - && rm -rf /var/lib/apt/lists/* - """; - - string baseLsioFilePath = Path.Combine(dockerPath, "NxBase-LSIO.Dockerfile"); - WriteDockerfile(baseLsioFilePath, baseLsioDockerfile); - } - - private static string CreateArgs( - ProductInfo.ProductType productType, - VersionInfo versionInfo, - bool lsio - ) => - // Args - $$""" - # Labels - ARG LABEL_NAME="{{ProductInfo.GetDocker(productType, lsio)}}" - ARG LABEL_DESCRIPTION="{{ProductInfo.GetDescription(productType)}}" - ARG LABEL_VERSION="{{versionInfo.Version}}" - - # Download URL and version - # Current values are defined by the build pipeline - ARG DOWNLOAD_X64_URL="{{versionInfo.UriX64}}" - ARG DOWNLOAD_ARM64_URL="{{versionInfo.UriArm64}}" - ARG DOWNLOAD_VERSION="{{versionInfo.Version}}" - - # Used for ${COMPANY_NAME} setting the server user and install directory - ARG RUNTIME_NAME="{{ProductInfo.GetCompany(productType)}}" - - # Global builder variables - # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope - ARG \ - # Platform of the build result. Eg linux/amd64, linux/arm/v7, windows/amd64 - TARGETPLATFORM \ - # Architecture component of TARGETPLATFORM - TARGETARCH \ - # Platform of the node performing the build - BUILDPLATFORM - - # Prevent EULA and confirmation prompts in installers - ARG DEBIAN_FRONTEND=noninteractive - - # Media server user and directory name - ENV COMPANY_NAME=${RUNTIME_NAME} - - # Labels - LABEL name=${LABEL_NAME}-${DOWNLOAD_VERSION} \ - description=${LABEL_DESCRIPTION} \ - version=${LABEL_VERSION} \ - maintainer="Pieter Viljoen <ptr727@users.noreply.github.com>" - - """; - - private static string CreateInstall(bool lsio) - { - // Install - string install = """ - # Download the installer file - WORKDIR /temp - COPY download.sh /temp/download.sh - RUN /bin/bash /temp/download.sh "${DOWNLOAD_X64_URL}" "${DOWNLOAD_ARM64_URL}" "${TARGETPLATFORM:-}" - - - """; - if (lsio) - { - install += """ - # Rename the LSIO "abc" user and group to ${COMPANY_NAME} - COPY lsio-rename-user.sh /temp/lsio-rename-user.sh - RUN /bin/bash /temp/lsio-rename-user.sh "${COMPANY_NAME}" - - - """; - } - - install += """ - # Install the mediaserver - RUN apt-get update \ - && apt-get install --no-install-recommends --yes \ - ./vms_server.deb \ - # Cleanup - && apt-get clean \ - && apt-get autoremove --purge --yes \ - && rm -rf /var/lib/apt/lists/* \ - && rm -rf /temp - - - """; - if (lsio) - { - install += """ - # Set ownership permissions - RUN chown --verbose ${COMPANY_NAME}:${COMPANY_NAME} /opt/${COMPANY_NAME}/mediaserver/bin \ - && chown --verbose ${COMPANY_NAME}:${COMPANY_NAME} /opt/${COMPANY_NAME}/mediaserver/bin/external.dat - - """; - } - else - { - install += """ - # Add the mediaserver ${COMPANY_NAME} user to the sudoers group - # Only allow sudo no password access to the root-tool - RUN echo "${COMPANY_NAME} ALL = NOPASSWD: /opt/${COMPANY_NAME}/mediaserver/bin/root-tool" > /etc/sudoers.d/${COMPANY_NAME} - - """; - } - - return install; - } - - private static string CreateEntrypoint(ProductInfo.ProductType productType, bool lsio) => - // Entrypoint - lsio - ? """ - # Copy etc init and services files - # https://www.linuxserver.io/blog/how-is-container-formed - COPY s6-overlay /etc/s6-overlay - - # Expose port 7001 - EXPOSE 7001 - - # Create mount points - # Config links will be created at runtime, see LSIO/etc/s6-overlay/s6-rc.d/init-nx-relocate/run - # /opt/${COMPANY_NAME}/mediaserver/etc -> /config/etc - # /opt/${COMPANY_NAME}/mediaserver/var -> /config/var - # /root/.config/nx_ini links -> /config/ini - # /config is for configuration - # /media is for recordings - # /backup is for backups - # /analytics is for analytics - VOLUME /config /media /backup /analytics - """ - : $$""" - # Copy the entrypoint.sh launch script - # entrypoint.sh will run the mediaserver and root-tool - COPY entrypoint.sh /opt/entrypoint.sh - RUN chmod +x /opt/entrypoint.sh - - # Run the entrypoint as the mediaserver ${COMPANY_NAME} user - # Note that this user exists in the container and does not directly map to a user on the host - USER ${COMPANY_NAME} - - # Runs entrypoint.sh on container start - ENTRYPOINT ["/opt/entrypoint.sh"] - - # Expose port 7001 - EXPOSE 7001 - - # Create mount points - # Link config directly to internal paths - # /mnt/config/etc:opt/{{ProductInfo.GetCompany( - productType - ).ToLowerInvariant()}}/mediaserver/etc - # /mnt/config/nx_ini:/home/{{ProductInfo.GetCompany( - productType - ).ToLowerInvariant()}}/.config/nx_ini - # /mnt/config/var:/opt/{{ProductInfo.GetCompany( - productType - ).ToLowerInvariant()}}/mediaserver/var - # /media is for recordings - # /backup is for backups - # /analytics is for analytics - VOLUME /media /backup /analytics - """; -} +namespace CreateMatrix; + +internal static class DockerFile +{ + private const string UbuntuBaseTag = "noble"; + private const string BaseImage = "docker.io/ptr727/nx-base:ubuntu-" + UbuntuBaseTag; + private const string BaseImageLsio = "docker.io/ptr727/nx-base-lsio:ubuntu-" + UbuntuBaseTag; + + public static void Create( + List<ProductInfo> productList, + string dockerPath, + VersionInfo.LabelType label + ) + { + CreateBaseDockerfiles(dockerPath); + + // Create a Docker file for each product type + foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) + { + // Find the matching product + ProductInfo productInfo = + productList.Find(item => item.Product == productType) + ?? throw new InvalidOperationException($"Product not found: {productType}"); + + // Get the version for the label, not all releases include Beta and RC labels + VersionInfo? versionInfo = productInfo.Versions.Find(item => + item.Labels.Contains(label) + ); + + // If the specific label is not found, use the latest version + if (versionInfo == null) + { + Log.Logger.Warning( + "Label {Label} not found for {Product}, using latest", + label, + productType + ); + versionInfo = productInfo.Versions.Find(item => + item.Labels.Contains(VersionInfo.LabelType.Latest) + ); + } + if (versionInfo == null) + { + throw new InvalidOperationException( + $"{productType}: No version found for label {label} or Latest" + ); + } + + // Create the standard Docker file + string dockerFile = CreateDockerfile(productType, versionInfo, false); + string filePath = Path.Combine( + dockerPath, + $"{ProductInfo.GetDocker(productType, false)}.Dockerfile" + ); + WriteDockerfile(filePath, dockerFile); + + // Create the LSIO Docker file + dockerFile = CreateDockerfile(productType, versionInfo, true); + filePath = Path.Combine( + dockerPath, + $"{ProductInfo.GetDocker(productType, true)}.Dockerfile" + ); + WriteDockerfile(filePath, dockerFile); + } + } + + private static void WriteDockerfile(string filePath, string dockerFile) + { + Log.Logger.Information("Writing Dockerfile to {Path}", filePath); + File.WriteAllText(filePath, dockerFile.ReplaceLineEndings("\n").TrimEnd('\n') + "\n"); + } + + private static string CreateDockerfile( + ProductInfo.ProductType productType, + VersionInfo versionInfo, + bool lsio + ) + { + // From + StringBuilder stringBuilder = new(); + _ = stringBuilder.AppendLineLf(CreateFrom(productType, lsio)); + + // Args + _ = stringBuilder.AppendLineLf(CreateArgs(productType, versionInfo, lsio)); + // Install + _ = stringBuilder.AppendLineLf(CreateInstall(lsio)); + + // Entrypoint + _ = stringBuilder.AppendLineLf(CreateEntrypoint(productType, lsio)); + + return stringBuilder.ToString(); + } + + private static string CreateFrom(ProductInfo.ProductType productType, bool lsio) + { + string from = $$""" + # Dockerfile created by CreateMatrix, do not modify by hand + # Product: {{productType}} + # Description: {{ProductInfo.GetDescription(productType)}} + # Company: {{ProductInfo.GetCompany(productType)}} + # Release: {{ProductInfo.GetRelease(productType)}} + # LSIO: {{lsio}} + + # https://support.networkoptix.com/hc/en-us/articles/205313168-Nx-Witness-Operating-System-Support + # Latest Ubuntu supported for v6 is Noble + # Base images are built in this repo, see Docker/NxBase*.Dockerfile + + """; + if (lsio) + { + from += $$""" + FROM {{BaseImageLsio}} + + """; + } + else + { + from += $$""" + FROM {{BaseImage}} + + """; + } + return from; + } + + private static void CreateBaseDockerfiles(string dockerPath) + { + string baseDockerfile = $$""" + # Base Dockerfile for Nx Witness images + # Built from ubuntu:{{UbuntuBaseTag}} + FROM ubuntu:{{UbuntuBaseTag}} + + # Prevent EULA and confirmation prompts in installers + ARG DEBIAN_FRONTEND=noninteractive + + # Common packages used by all product images + # https://github.com/ptr727/NxWitness/issues/282 + RUN apt-get update \ + && apt-get upgrade --yes \ + && apt-get install --no-install-recommends --yes \ + ca-certificates \ + gdb \ + libdrm2 \ + sudo \ + unzip \ + wget \ + && apt-get clean \ + && apt-get autoremove --purge --yes \ + && rm -rf /var/lib/apt/lists/* + """; + + string baseFilePath = Path.Combine(dockerPath, "NxBase.Dockerfile"); + WriteDockerfile(baseFilePath, baseDockerfile); + + string baseLsioDockerfile = $$""" + # Base Dockerfile for Nx Witness LSIO images + # Built from lsiobase/ubuntu:{{UbuntuBaseTag}} + FROM lsiobase/ubuntu:{{UbuntuBaseTag}} + + # Prevent EULA and confirmation prompts in installers + ARG DEBIAN_FRONTEND=noninteractive + + # Common packages used by all product images + # https://github.com/ptr727/NxWitness/issues/282 + RUN apt-get update \ + && apt-get upgrade --yes \ + && apt-get install --no-install-recommends --yes \ + ca-certificates \ + gdb \ + libdrm2 \ + unzip \ + wget \ + && apt-get clean \ + && apt-get autoremove --purge --yes \ + && rm -rf /var/lib/apt/lists/* + """; + + string baseLsioFilePath = Path.Combine(dockerPath, "NxBase-LSIO.Dockerfile"); + WriteDockerfile(baseLsioFilePath, baseLsioDockerfile); + } + + private static string CreateArgs( + ProductInfo.ProductType productType, + VersionInfo versionInfo, + bool lsio + ) => + // Args + $$""" + # Labels + ARG LABEL_NAME="{{ProductInfo.GetDocker(productType, lsio)}}" + ARG LABEL_DESCRIPTION="{{ProductInfo.GetDescription(productType)}}" + ARG LABEL_VERSION="{{versionInfo.Version}}" + + # Download URL and version + # Current values are defined by the build pipeline + ARG DOWNLOAD_X64_URL="{{versionInfo.UriX64}}" + ARG DOWNLOAD_ARM64_URL="{{versionInfo.UriArm64}}" + ARG DOWNLOAD_VERSION="{{versionInfo.Version}}" + + # Used for ${COMPANY_NAME} setting the server user and install directory + ARG RUNTIME_NAME="{{ProductInfo.GetCompany(productType)}}" + + # Global builder variables + # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope + ARG \ + # Platform of the build result. Eg linux/amd64, linux/arm/v7, windows/amd64 + TARGETPLATFORM \ + # Architecture component of TARGETPLATFORM + TARGETARCH \ + # Platform of the node performing the build + BUILDPLATFORM + + # Prevent EULA and confirmation prompts in installers + ARG DEBIAN_FRONTEND=noninteractive + + # Media server user and directory name + ENV COMPANY_NAME=${RUNTIME_NAME} + + # Labels + LABEL name=${LABEL_NAME}-${DOWNLOAD_VERSION} \ + description=${LABEL_DESCRIPTION} \ + version=${LABEL_VERSION} \ + maintainer="Pieter Viljoen <ptr727@users.noreply.github.com>" + + """; + + private static string CreateInstall(bool lsio) + { + // Install + string install = """ + # Download the installer file + WORKDIR /temp + COPY download.sh /temp/download.sh + RUN /bin/bash /temp/download.sh "${DOWNLOAD_X64_URL}" "${DOWNLOAD_ARM64_URL}" "${TARGETPLATFORM:-}" + + + """; + if (lsio) + { + install += """ + # Rename the LSIO "abc" user and group to ${COMPANY_NAME} + COPY lsio-rename-user.sh /temp/lsio-rename-user.sh + RUN /bin/bash /temp/lsio-rename-user.sh "${COMPANY_NAME}" + + + """; + } + + install += """ + # Install the mediaserver + RUN apt-get update \ + && apt-get install --no-install-recommends --yes \ + ./vms_server.deb \ + # Cleanup + && apt-get clean \ + && apt-get autoremove --purge --yes \ + && rm -rf /var/lib/apt/lists/* \ + && rm -rf /temp + + + """; + if (lsio) + { + install += """ + # Set ownership permissions + RUN chown --verbose ${COMPANY_NAME}:${COMPANY_NAME} /opt/${COMPANY_NAME}/mediaserver/bin \ + && chown --verbose ${COMPANY_NAME}:${COMPANY_NAME} /opt/${COMPANY_NAME}/mediaserver/bin/external.dat + + """; + } + else + { + install += """ + # Add the mediaserver ${COMPANY_NAME} user to the sudoers group + # Only allow sudo no password access to the root-tool + RUN echo "${COMPANY_NAME} ALL = NOPASSWD: /opt/${COMPANY_NAME}/mediaserver/bin/root-tool" > /etc/sudoers.d/${COMPANY_NAME} + + """; + } + + return install; + } + + private static string CreateEntrypoint(ProductInfo.ProductType productType, bool lsio) => + // Entrypoint + lsio + ? """ + # Copy etc init and services files + # https://www.linuxserver.io/blog/how-is-container-formed + COPY s6-overlay /etc/s6-overlay + + # Expose port 7001 + EXPOSE 7001 + + # Create mount points + # Config links will be created at runtime, see Docker/s6-overlay/s6-rc.d/init-nx-relocate/run + # /opt/${COMPANY_NAME}/mediaserver/etc -> /config/etc + # /opt/${COMPANY_NAME}/mediaserver/var -> /config/var + # /root/.config/nx_ini links -> /config/ini + # /config is for configuration + # /media is for recordings + # /backup is for backups + # /analytics is for analytics + VOLUME /config /media /backup /analytics + """ + : $$""" + # Copy the entrypoint.sh launch script + # entrypoint.sh will run the mediaserver and root-tool + COPY entrypoint.sh /opt/entrypoint.sh + RUN chmod +x /opt/entrypoint.sh + + # Run the entrypoint as the mediaserver ${COMPANY_NAME} user + # Note that this user exists in the container and does not directly map to a user on the host + USER ${COMPANY_NAME} + + # Runs entrypoint.sh on container start + ENTRYPOINT ["/opt/entrypoint.sh"] + + # Expose port 7001 + EXPOSE 7001 + + # Create mount points + # Link config directly to internal paths + # /mnt/config/etc:opt/{{ProductInfo.GetCompany( + productType + ).ToLowerInvariant()}}/mediaserver/etc + # /mnt/config/nx_ini:/home/{{ProductInfo.GetCompany( + productType + ).ToLowerInvariant()}}/.config/nx_ini + # /mnt/config/var:/opt/{{ProductInfo.GetCompany( + productType + ).ToLowerInvariant()}}/mediaserver/var + # /media is for recordings + # /backup is for backups + # /analytics is for analytics + VOLUME /media /backup /analytics + """; +} diff --git a/CreateMatrix/Extensions.cs b/CreateMatrix/Extensions.cs index 850ea96e..f159c5f1 100644 --- a/CreateMatrix/Extensions.cs +++ b/CreateMatrix/Extensions.cs @@ -1,43 +1,43 @@ -using System.Runtime.CompilerServices; - -namespace CreateMatrix; - -internal static class LogExtensions -{ - extension(ILogger logger) - { - internal bool LogAndPropagate( - Exception exception, - [CallerMemberName] string function = "unknown" - ) - { - logger.Error(exception, "{Function}", function); - return false; - } - - internal bool LogAndHandle( - Exception exception, - [CallerMemberName] string function = "unknown" - ) - { - logger.Error(exception, "{Function}", function); - return true; - } - } -} - -internal static class StringBuilderExtensions -{ - extension(StringBuilder sb) - { - internal StringBuilder AppendLineCrlf(string? value = null) - { - if (value is not null) - { - _ = sb.Append(value); - } - - return sb.Append("\r\n"); - } - } -} +using System.Runtime.CompilerServices; + +namespace CreateMatrix; + +internal static class LogExtensions +{ + extension(ILogger logger) + { + internal bool LogAndPropagate( + Exception exception, + [CallerMemberName] string function = "unknown" + ) + { + logger.Error(exception, "{Function}", function); + return false; + } + + internal bool LogAndHandle( + Exception exception, + [CallerMemberName] string function = "unknown" + ) + { + logger.Error(exception, "{Function}", function); + return true; + } + } +} + +internal static class StringBuilderExtensions +{ + extension(StringBuilder sb) + { + internal StringBuilder AppendLineLf(string? value = null) + { + if (value is not null) + { + _ = sb.Append(value); + } + + return sb.Append('\n'); + } + } +} diff --git a/CreateMatrix/GlobalUsings.cs b/CreateMatrix/GlobalUsings.cs index 71c49a03..0ac7a04c 100644 --- a/CreateMatrix/GlobalUsings.cs +++ b/CreateMatrix/GlobalUsings.cs @@ -1,14 +1,14 @@ -global using System; -global using System.Collections.Generic; -global using System.ComponentModel; -global using System.Diagnostics; -global using System.Globalization; -global using System.IO; -global using System.Linq; -global using System.Net.Http; -global using System.Text; -global using System.Text.Json; -global using System.Text.Json.Serialization; -global using System.Threading; -global using System.Threading.Tasks; -global using Serilog; +global using System; +global using System.Collections.Generic; +global using System.ComponentModel; +global using System.Diagnostics; +global using System.Globalization; +global using System.IO; +global using System.Linq; +global using System.Net.Http; +global using System.Text; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Threading; +global using System.Threading.Tasks; +global using Serilog; diff --git a/CreateMatrix/HttpClientFactory.cs b/CreateMatrix/HttpClientFactory.cs index 76fd050c..075db4da 100644 --- a/CreateMatrix/HttpClientFactory.cs +++ b/CreateMatrix/HttpClientFactory.cs @@ -1,69 +1,69 @@ -using System.Net.Http.Headers; -using Microsoft.Extensions.Http.Resilience; -using Polly; - -namespace CreateMatrix; - -internal static class HttpClientFactory -{ - private static readonly Lazy<HttpClient> s_httpClient = new(CreateHttpClient); - private static readonly Lazy<ResilienceHandler> s_resilienceHandler = new( - CreateResilienceHandler - ); - - internal static HttpClient GetHttpClient() => s_httpClient.Value; - - private static ResilienceHandler GetResilienceHandler() => s_resilienceHandler.Value; - - private static ResilienceHandler CreateResilienceHandler() => - new( - new ResiliencePipelineBuilder<HttpResponseMessage>() - .AddRetry( - new Polly.Retry.RetryStrategyOptions<HttpResponseMessage> - { - MaxRetryAttempts = 3, - BackoffType = DelayBackoffType.Exponential, - UseJitter = true, - Delay = TimeSpan.FromSeconds(1), - MaxDelay = TimeSpan.FromSeconds(30), - ShouldHandle = args => - ValueTask.FromResult( - args.Outcome.Exception != null - || args.Outcome.Result is { IsSuccessStatusCode: false } - ), - } - ) - .AddCircuitBreaker( - new Polly.CircuitBreaker.CircuitBreakerStrategyOptions<HttpResponseMessage> - { - FailureRatio = 0.2, - MinimumThroughput = 10, - SamplingDuration = TimeSpan.FromSeconds(60), - BreakDuration = TimeSpan.FromSeconds(30), - ShouldHandle = args => - ValueTask.FromResult( - args.Outcome.Exception != null - || args.Outcome.Result is { IsSuccessStatusCode: false } - ), - } - ) - .AddTimeout(TimeSpan.FromSeconds(30)) - .Build() - ) - { - InnerHandler = new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(15), - PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), - }, - }; - - private static HttpClient CreateHttpClient() - { - HttpClient httpClient = new(GetResilienceHandler()) { Timeout = TimeSpan.FromSeconds(120) }; - httpClient.DefaultRequestHeaders.UserAgent.Add( - new ProductInfoHeaderValue(AssemblyInfo.AppName, AssemblyInfo.InformationalVersion) - ); - return httpClient; - } -} +using System.Net.Http.Headers; +using Microsoft.Extensions.Http.Resilience; +using Polly; + +namespace CreateMatrix; + +internal static class HttpClientFactory +{ + private static readonly Lazy<HttpClient> s_httpClient = new(CreateHttpClient); + private static readonly Lazy<ResilienceHandler> s_resilienceHandler = new( + CreateResilienceHandler + ); + + internal static HttpClient GetHttpClient() => s_httpClient.Value; + + private static ResilienceHandler GetResilienceHandler() => s_resilienceHandler.Value; + + private static ResilienceHandler CreateResilienceHandler() => + new( + new ResiliencePipelineBuilder<HttpResponseMessage>() + .AddRetry( + new Polly.Retry.RetryStrategyOptions<HttpResponseMessage> + { + MaxRetryAttempts = 3, + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, + Delay = TimeSpan.FromSeconds(1), + MaxDelay = TimeSpan.FromSeconds(30), + ShouldHandle = args => + ValueTask.FromResult( + args.Outcome.Exception != null + || args.Outcome.Result is { IsSuccessStatusCode: false } + ), + } + ) + .AddCircuitBreaker( + new Polly.CircuitBreaker.CircuitBreakerStrategyOptions<HttpResponseMessage> + { + FailureRatio = 0.2, + MinimumThroughput = 10, + SamplingDuration = TimeSpan.FromSeconds(60), + BreakDuration = TimeSpan.FromSeconds(30), + ShouldHandle = args => + ValueTask.FromResult( + args.Outcome.Exception != null + || args.Outcome.Result is { IsSuccessStatusCode: false } + ), + } + ) + .AddTimeout(TimeSpan.FromSeconds(30)) + .Build() + ) + { + InnerHandler = new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), + }, + }; + + private static HttpClient CreateHttpClient() + { + HttpClient httpClient = new(GetResilienceHandler()) { Timeout = TimeSpan.FromSeconds(120) }; + httpClient.DefaultRequestHeaders.UserAgent.Add( + new ProductInfoHeaderValue(AssemblyInfo.AppName, AssemblyInfo.InformationalVersion) + ); + return httpClient; + } +} diff --git a/CreateMatrix/ImageInfo.cs b/CreateMatrix/ImageInfo.cs index 1a533b3d..f83fb784 100644 --- a/CreateMatrix/ImageInfo.cs +++ b/CreateMatrix/ImageInfo.cs @@ -1,144 +1,144 @@ -using System.Collections.Immutable; - -namespace CreateMatrix; - -internal class ImageInfo -{ - public enum BranchType - { - None, - Main, - Develop, - } - - public enum BaseType - { - None, - Ubuntu, - LSIO, - } - - public string Name { get; set; } = string.Empty; - - [JsonConverter(typeof(LowercaseEnumConverter<ProductInfo.ProductType>))] - public ProductInfo.ProductType Product { get; set; } = ProductInfo.ProductType.None; - - [JsonConverter(typeof(LowercaseEnumConverter<BranchType>))] - public BranchType Branch { get; set; } = BranchType.None; - - [JsonConverter(typeof(LowercaseEnumConverter<BaseType>))] - public BaseType Base { get; set; } = BaseType.None; - - public List<string> Tags { get; set; } = []; - public List<string> Args { get; set; } = []; - - private const string Registry = "docker.io/ptr727"; - - private ImageInfo(ProductInfo.ProductType productType, BranchType branchType, BaseType baseType) - { - Product = productType; - Branch = branchType; - Base = baseType; - // The image name matches the Dockerfile name - Name = ProductInfo.GetDocker(productType, baseType == BaseType.LSIO); - } - - private void AddArgs(VersionInfo versionInfo) - { - Args.Add($"{DownloadVersion}={versionInfo.Version}"); - Args.Add($"{DownloadX64Url}={versionInfo.UriX64}"); - Args.Add($"{DownloadArm64Url}={versionInfo.UriArm64}"); - } - - private void AddTag(string tag) - { - if (Branch == BranchType.Develop) - { - tag = $"develop-{tag}"; - } - Tags.Add($"{Registry}/{Name.ToLowerInvariant()}:{tag.ToLowerInvariant()}"); - } - - private void AddTag(VersionInfo.LabelType labelType) - { - string tag = labelType.ToString().ToLowerInvariant(); - if (Branch == BranchType.Develop) - { - tag = labelType == VersionInfo.LabelType.Latest ? $"develop" : $"develop-{tag}"; - } - Tags.Add($"{Registry}/{Name.ToLowerInvariant()}:{tag.ToLowerInvariant()}"); - } - - public static List<ImageInfo> CreateImages(List<ProductInfo> productList) - { - // Create images for all products - List<ImageInfo> imageList = []; - productList.ForEach(product => - { - imageList.AddRange(CreateImages(product, BaseType.Ubuntu, BranchType.Main)); - imageList.AddRange(CreateImages(product, BaseType.Ubuntu, BranchType.Develop)); - imageList.AddRange(CreateImages(product, BaseType.LSIO, BranchType.Main)); - imageList.AddRange(CreateImages(product, BaseType.LSIO, BranchType.Develop)); - }); - - return imageList; - } - - private static List<ImageInfo> CreateImages( - ProductInfo productInfo, - BaseType baseType, - BranchType branchType - ) - { - // Create a set by unique versions - ImmutableSortedSet<VersionInfo> versionSet = productInfo.Versions.ToImmutableSortedSet( - new VersionInfoComparer() - ); - Debug.Assert(versionSet.Count == productInfo.Versions.Count); - - // Create images for each version - List<ImageInfo> imageList = []; - foreach (VersionInfo version in versionSet) - { - // Create image - ImageInfo imageInfo = new(productInfo.Product, branchType, baseType); - - // Add a tag for the version - imageInfo.AddTag(version.Version); - - // Add tags for all labels - version.Labels.ForEach(imageInfo.AddTag); - - // Add args - imageInfo.AddArgs(version); - - // Sort args and tags to make diffs easier - imageInfo.Args.Sort(); - imageInfo.Tags.Sort(); - - // Add image to list - imageList.Add(imageInfo); - } - - // Done - return imageList; - } - - public void LogInformation() - { - Log.Logger.Information( - "Name: {Name}, Base: {Base}, Branch: {Branch}, Tags: {Tags}, Args: {Args}", - Name, - Base, - Branch, - Tags.Count, - Args.Count - ); - Tags.ForEach(item => Log.Logger.Information("Name: {Name}, Tag: {Tag}", Name, item)); - Args.ForEach(item => Log.Logger.Information("Name: {Name}, Arg: {Arg}", Name, item)); - } - - private const string DownloadVersion = "DOWNLOAD_VERSION"; - private const string DownloadX64Url = "DOWNLOAD_X64_URL"; - private const string DownloadArm64Url = "DOWNLOAD_ARM64_URL"; -} +using System.Collections.Immutable; + +namespace CreateMatrix; + +internal class ImageInfo +{ + public enum BranchType + { + None, + Main, + Develop, + } + + public enum BaseType + { + None, + Ubuntu, + LSIO, + } + + public string Name { get; set; } = string.Empty; + + [JsonConverter(typeof(LowercaseEnumConverter<ProductInfo.ProductType>))] + public ProductInfo.ProductType Product { get; set; } = ProductInfo.ProductType.None; + + [JsonConverter(typeof(LowercaseEnumConverter<BranchType>))] + public BranchType Branch { get; set; } = BranchType.None; + + [JsonConverter(typeof(LowercaseEnumConverter<BaseType>))] + public BaseType Base { get; set; } = BaseType.None; + + public List<string> Tags { get; set; } = []; + public List<string> Args { get; set; } = []; + + private const string Registry = "docker.io/ptr727"; + + private ImageInfo(ProductInfo.ProductType productType, BranchType branchType, BaseType baseType) + { + Product = productType; + Branch = branchType; + Base = baseType; + // The image name matches the Dockerfile name + Name = ProductInfo.GetDocker(productType, baseType == BaseType.LSIO); + } + + private void AddArgs(VersionInfo versionInfo) + { + Args.Add($"{DownloadVersion}={versionInfo.Version}"); + Args.Add($"{DownloadX64Url}={versionInfo.UriX64}"); + Args.Add($"{DownloadArm64Url}={versionInfo.UriArm64}"); + } + + private void AddTag(string tag) + { + if (Branch == BranchType.Develop) + { + tag = $"develop-{tag}"; + } + Tags.Add($"{Registry}/{Name.ToLowerInvariant()}:{tag.ToLowerInvariant()}"); + } + + private void AddTag(VersionInfo.LabelType labelType) + { + string tag = labelType.ToString().ToLowerInvariant(); + if (Branch == BranchType.Develop) + { + tag = labelType == VersionInfo.LabelType.Latest ? $"develop" : $"develop-{tag}"; + } + Tags.Add($"{Registry}/{Name.ToLowerInvariant()}:{tag.ToLowerInvariant()}"); + } + + public static List<ImageInfo> CreateImages(List<ProductInfo> productList) + { + // Create images for all products + List<ImageInfo> imageList = []; + productList.ForEach(product => + { + imageList.AddRange(CreateImages(product, BaseType.Ubuntu, BranchType.Main)); + imageList.AddRange(CreateImages(product, BaseType.Ubuntu, BranchType.Develop)); + imageList.AddRange(CreateImages(product, BaseType.LSIO, BranchType.Main)); + imageList.AddRange(CreateImages(product, BaseType.LSIO, BranchType.Develop)); + }); + + return imageList; + } + + private static List<ImageInfo> CreateImages( + ProductInfo productInfo, + BaseType baseType, + BranchType branchType + ) + { + // Create a set by unique versions + ImmutableSortedSet<VersionInfo> versionSet = productInfo.Versions.ToImmutableSortedSet( + new VersionInfoComparer() + ); + Debug.Assert(versionSet.Count == productInfo.Versions.Count); + + // Create images for each version + List<ImageInfo> imageList = []; + foreach (VersionInfo version in versionSet) + { + // Create image + ImageInfo imageInfo = new(productInfo.Product, branchType, baseType); + + // Add a tag for the version + imageInfo.AddTag(version.Version); + + // Add tags for all labels + version.Labels.ForEach(imageInfo.AddTag); + + // Add args + imageInfo.AddArgs(version); + + // Sort args and tags to make diffs easier + imageInfo.Args.Sort(); + imageInfo.Tags.Sort(); + + // Add image to list + imageList.Add(imageInfo); + } + + // Done + return imageList; + } + + public void LogInformation() + { + Log.Logger.Information( + "Name: {Name}, Base: {Base}, Branch: {Branch}, Tags: {Tags}, Args: {Args}", + Name, + Base, + Branch, + Tags.Count, + Args.Count + ); + Tags.ForEach(item => Log.Logger.Information("Name: {Name}, Tag: {Tag}", Name, item)); + Args.ForEach(item => Log.Logger.Information("Name: {Name}, Arg: {Arg}", Name, item)); + } + + private const string DownloadVersion = "DOWNLOAD_VERSION"; + private const string DownloadX64Url = "DOWNLOAD_X64_URL"; + private const string DownloadArm64Url = "DOWNLOAD_ARM64_URL"; +} diff --git a/CreateMatrix/MatrixJsonSchema.cs b/CreateMatrix/MatrixJsonSchema.cs index 61ff55c7..60bf1392 100644 --- a/CreateMatrix/MatrixJsonSchema.cs +++ b/CreateMatrix/MatrixJsonSchema.cs @@ -1,61 +1,61 @@ -namespace CreateMatrix; - -internal class MatrixJsonSchemaBase -{ - [JsonRequired] - [JsonPropertyOrder(-2)] - public int SchemaVersion { get; set; } = MatrixJsonSchema.Version; -} - -internal class MatrixJsonSchema : MatrixJsonSchemaBase -{ - public const int Version = 2; - - [JsonRequired] - public List<ImageInfo> Images { get; set; } = []; - - public static void ToFile(string path, MatrixJsonSchema json) - { - json.SchemaVersion = Version; - File.WriteAllText(path, ToJson(json)); - } - - private static string ToJson(MatrixJsonSchema json) => - JsonSerializer.Serialize(json, MatrixJsonContext.Default.MatrixJsonSchema); -} - -internal sealed class LowercaseEnumConverter<T> : JsonConverter<T> - where T : struct, Enum -{ - public override T Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options - ) - { - string? value = - reader.GetString() ?? throw new JsonException($"Expected {typeof(T).Name} value."); - - return !Enum.TryParse(value, true, out T result) - ? throw new JsonException($"Invalid {typeof(T).Name} value '{value}'.") - : result; - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => - writer.WriteStringValue(value.ToString().ToLowerInvariant()); -} - -[JsonSourceGenerationOptions( - AllowTrailingCommas = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - IncludeFields = true, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, - ReadCommentHandling = JsonCommentHandling.Skip, - UseStringEnumConverter = true, - WriteIndented = true, - NewLine = "\r\n" -)] -[JsonSerializable(typeof(MatrixJsonSchema))] -[JsonSerializable(typeof(MatrixJsonSchemaBase))] -internal partial class MatrixJsonContext : JsonSerializerContext; +namespace CreateMatrix; + +internal class MatrixJsonSchemaBase +{ + [JsonRequired] + [JsonPropertyOrder(-2)] + public int SchemaVersion { get; set; } = MatrixJsonSchema.Version; +} + +internal class MatrixJsonSchema : MatrixJsonSchemaBase +{ + public const int Version = 2; + + [JsonRequired] + public List<ImageInfo> Images { get; set; } = []; + + public static void ToFile(string path, MatrixJsonSchema json) + { + json.SchemaVersion = Version; + File.WriteAllText(path, ToJson(json)); + } + + private static string ToJson(MatrixJsonSchema json) => + JsonSerializer.Serialize(json, MatrixJsonContext.Default.MatrixJsonSchema); +} + +internal sealed class LowercaseEnumConverter<T> : JsonConverter<T> + where T : struct, Enum +{ + public override T Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + string? value = + reader.GetString() ?? throw new JsonException($"Expected {typeof(T).Name} value."); + + return !Enum.TryParse(value, true, out T result) + ? throw new JsonException($"Invalid {typeof(T).Name} value '{value}'.") + : result; + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => + writer.WriteStringValue(value.ToString().ToLowerInvariant()); +} + +[JsonSourceGenerationOptions( + AllowTrailingCommas = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, + ReadCommentHandling = JsonCommentHandling.Skip, + UseStringEnumConverter = true, + WriteIndented = true, + NewLine = "\n" +)] +[JsonSerializable(typeof(MatrixJsonSchema))] +[JsonSerializable(typeof(MatrixJsonSchemaBase))] +internal partial class MatrixJsonContext : JsonSerializerContext; diff --git a/CreateMatrix/PackagesJsonSchema.cs b/CreateMatrix/PackagesJsonSchema.cs index ad59f1ae..239183b2 100644 --- a/CreateMatrix/PackagesJsonSchema.cs +++ b/CreateMatrix/PackagesJsonSchema.cs @@ -1,104 +1,104 @@ -namespace CreateMatrix; - -// https://updates.networkoptix.com/{product}/{build}/packages.json -// https://updates.networkoptix.com/metavms/35134/packages.json -// https://updates.networkoptix.com/default/35270/packages.json -// https://updates.networkoptix.com/digitalwatchdog/35271/packages.json - -internal sealed class Variant -{ - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; -} - -internal sealed class Package -{ - [JsonPropertyName("component")] - public string Component { get; set; } = string.Empty; - - [JsonPropertyName("platform")] - public string PlatformName { get; set; } = string.Empty; - - [JsonPropertyName("file")] - public string File { get; set; } = string.Empty; - - [JsonPropertyName("variants")] - public List<Variant> Variants { get; set; } = []; - - public bool IsX64Server() => - // Test for Server and x64 and Ubuntu - Component.Equals("server", StringComparison.OrdinalIgnoreCase) - && PlatformName.Equals("linux_x64", StringComparison.OrdinalIgnoreCase) - && Variants.Any(variant => - variant.Name.Equals("ubuntu", StringComparison.OrdinalIgnoreCase) - ); - - public bool IsArm64Server() => - // Test for Server and Arm64 and Ubuntu - Component.Equals("server", StringComparison.OrdinalIgnoreCase) - && PlatformName.Equals("linux_arm64", StringComparison.OrdinalIgnoreCase) - && Variants.Any(variant => - variant.Name.Equals("ubuntu", StringComparison.OrdinalIgnoreCase) - ); -} - -internal sealed class PackagesJsonSchema -{ - [JsonPropertyName("packages")] - public List<Package> Packages { get; set; } = []; - - private static PackagesJsonSchema FromJson(string json) - { - PackagesJsonSchema? jsonSchema = JsonSerializer.Deserialize( - json, - PackagesJsonContext.Default.PackagesJsonSchema - ); - ArgumentNullException.ThrowIfNull(jsonSchema); - return jsonSchema; - } - - public static async Task<List<Package>> GetPackagesAsync( - string releaseName, - int buildNumber, - CancellationToken cancellationToken - ) - { - // Load packages JSON - // https://updates.networkoptix.com/{product}/{build}/packages.json - ArgumentNullException.ThrowIfNull(releaseName); - - HttpClient httpClient = HttpClientFactory.GetHttpClient(); - Uri packagesUri = new( - $"https://updates.networkoptix.com/{releaseName}/{buildNumber}/packages.json" - ); - Log.Logger.Information("Getting package information from {Uri}", packagesUri); - string jsonString = await httpClient - .GetStringAsync(packagesUri, cancellationToken) - .ConfigureAwait(false); - - // Deserialize JSON - PackagesJsonSchema packagesSchema = FromJson(jsonString); - ArgumentNullException.ThrowIfNull(packagesSchema); - if (packagesSchema.Packages.Count == 0) - { - throw new InvalidOperationException($"No packages found in {packagesUri}"); - } - - // Return packages - return packagesSchema.Packages; - } -} - -[JsonSourceGenerationOptions( - AllowTrailingCommas = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - IncludeFields = true, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, - ReadCommentHandling = JsonCommentHandling.Skip, - UseStringEnumConverter = true, - WriteIndented = true, - NewLine = "\r\n" -)] -[JsonSerializable(typeof(PackagesJsonSchema))] -internal partial class PackagesJsonContext : JsonSerializerContext; +namespace CreateMatrix; + +// https://updates.networkoptix.com/{product}/{build}/packages.json +// https://updates.networkoptix.com/metavms/35134/packages.json +// https://updates.networkoptix.com/default/35270/packages.json +// https://updates.networkoptix.com/digitalwatchdog/35271/packages.json + +internal sealed class Variant +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +internal sealed class Package +{ + [JsonPropertyName("component")] + public string Component { get; set; } = string.Empty; + + [JsonPropertyName("platform")] + public string PlatformName { get; set; } = string.Empty; + + [JsonPropertyName("file")] + public string File { get; set; } = string.Empty; + + [JsonPropertyName("variants")] + public List<Variant> Variants { get; set; } = []; + + public bool IsX64Server() => + // Test for Server and x64 and Ubuntu + Component.Equals("server", StringComparison.OrdinalIgnoreCase) + && PlatformName.Equals("linux_x64", StringComparison.OrdinalIgnoreCase) + && Variants.Any(variant => + variant.Name.Equals("ubuntu", StringComparison.OrdinalIgnoreCase) + ); + + public bool IsArm64Server() => + // Test for Server and Arm64 and Ubuntu + Component.Equals("server", StringComparison.OrdinalIgnoreCase) + && PlatformName.Equals("linux_arm64", StringComparison.OrdinalIgnoreCase) + && Variants.Any(variant => + variant.Name.Equals("ubuntu", StringComparison.OrdinalIgnoreCase) + ); +} + +internal sealed class PackagesJsonSchema +{ + [JsonPropertyName("packages")] + public List<Package> Packages { get; set; } = []; + + private static PackagesJsonSchema FromJson(string json) + { + PackagesJsonSchema? jsonSchema = JsonSerializer.Deserialize( + json, + PackagesJsonContext.Default.PackagesJsonSchema + ); + ArgumentNullException.ThrowIfNull(jsonSchema); + return jsonSchema; + } + + public static async Task<List<Package>> GetPackagesAsync( + string releaseName, + int buildNumber, + CancellationToken cancellationToken + ) + { + // Load packages JSON + // https://updates.networkoptix.com/{product}/{build}/packages.json + ArgumentNullException.ThrowIfNull(releaseName); + + HttpClient httpClient = HttpClientFactory.GetHttpClient(); + Uri packagesUri = new( + $"https://updates.networkoptix.com/{releaseName}/{buildNumber}/packages.json" + ); + Log.Logger.Information("Getting package information from {Uri}", packagesUri); + string jsonString = await httpClient + .GetStringAsync(packagesUri, cancellationToken) + .ConfigureAwait(false); + + // Deserialize JSON + PackagesJsonSchema packagesSchema = FromJson(jsonString); + ArgumentNullException.ThrowIfNull(packagesSchema); + if (packagesSchema.Packages.Count == 0) + { + throw new InvalidOperationException($"No packages found in {packagesUri}"); + } + + // Return packages + return packagesSchema.Packages; + } +} + +[JsonSourceGenerationOptions( + AllowTrailingCommas = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, + ReadCommentHandling = JsonCommentHandling.Skip, + UseStringEnumConverter = true, + WriteIndented = true, + NewLine = "\n" +)] +[JsonSerializable(typeof(PackagesJsonSchema))] +internal partial class PackagesJsonContext : JsonSerializerContext; diff --git a/CreateMatrix/ProductInfo.cs b/CreateMatrix/ProductInfo.cs index 8746a534..8f2f1e5f 100644 --- a/CreateMatrix/ProductInfo.cs +++ b/CreateMatrix/ProductInfo.cs @@ -1,440 +1,440 @@ -namespace CreateMatrix; - -internal sealed class ProductInfo -{ - public enum ProductType - { - None, - NxGo, - NxMeta, - NxWitness, - DWSpectrum, - WisenetWAVE, - } - - public ProductType Product { get; set; } - - public List<VersionInfo> Versions { get; set; } = []; - - public string GetCompany() => GetCompany(Product); - - public string GetRelease() => GetRelease(Product); - - public string GetDescription() => GetDescription(Product); - - public string GetDocker(bool lsio) => GetDocker(Product, lsio); - - // Used for release JSON API https://updates.vmsproxy.com/{release}/releases.json path - public static string GetRelease(ProductType productType) => - productType switch - { - ProductType.NxGo => "nxgo", - ProductType.NxMeta => "metavms", - ProductType.NxWitness => "default", - ProductType.DWSpectrum => "digitalwatchdog", - ProductType.WisenetWAVE => "hanwha", - ProductType.None => throw new InvalidOperationException( - $"{nameof(ProductType)} is None" - ), - _ => throw new InvalidEnumArgumentException( - nameof(productType), - (int)productType, - typeof(ProductType) - ), - }; - - // Used for ${COMPANY_NAME} mediaserver install path and user account - public static string GetCompany(ProductType productType) => - productType switch - { - ProductType.NxGo => "networkoptix", - ProductType.NxMeta => "networkoptix-metavms", - ProductType.NxWitness => "networkoptix", - ProductType.DWSpectrum => "digitalwatchdog", - ProductType.WisenetWAVE => "hanwha", - ProductType.None => throw new InvalidOperationException( - $"{nameof(ProductType)} is None" - ), - _ => throw new InvalidEnumArgumentException( - nameof(productType), - (int)productType, - typeof(ProductType) - ), - }; - - // Used for ${LABEL_DESCRIPTION} in Dockerfile - public static string GetDescription(ProductType productType) => - productType switch - { - ProductType.NxGo => "Nx Go VMS", - ProductType.NxMeta => "Nx Meta VMS", - ProductType.NxWitness => "Nx Witness VMS", - ProductType.DWSpectrum => "DW Spectrum IPVMS", - ProductType.WisenetWAVE => "Wisenet WAVE VMS", - ProductType.None => throw new InvalidOperationException( - $"{nameof(ProductType)} is None" - ), - _ => throw new InvalidEnumArgumentException( - nameof(productType), - (int)productType, - typeof(ProductType) - ), - }; - - // Dockerfile name, excluding the .Dockerfile extension. - // This is the single source of the image/Dockerfile naming convention. ImageInfo derives its - // name from here. The base variant is a bool (Ubuntu vs LSIO). Promote it to an enum only if a - // third base type is ever added (which would also ripple through ComposeFile/DockerFile). - public static string GetDocker(ProductType productType, bool lsio) => - $"{productType}{(lsio ? "-LSIO" : "")}"; - - public static IEnumerable<ProductType> GetProductTypes() => - // Create list of product types - [.. Enum.GetValues<ProductType>().Where(productType => productType != ProductType.None)]; - - public static List<ProductInfo> GetProducts() => - // Create list of all known products - [ - .. from ProductType productType in GetProductTypes() - select new ProductInfo { Product = productType }, - ]; - - public async Task FetchVersionsAsync(CancellationToken cancellationToken) - { - // Get version information using releases.json and package.json - Log.Logger.Information("{Product}: Getting online release information...", Product); - try - { - // Get all releases - List<Release> releasesList = await ReleasesJsonSchema - .GetReleasesAsync(GetRelease(), cancellationToken) - .ConfigureAwait(false); - foreach (Release release in releasesList) - { - // Only process "vms" products - if (!release.Product.Equals(Release.VmsProduct, StringComparison.OrdinalIgnoreCase)) - { - Log.Logger.Warning( - "{Product}: Skipping {ReleaseProduct}", - Product, - release.Product - ); - continue; - } - - // Set the version and label - VersionInfo versionInfo = CreateVersionInfo(release); - - // Get the build number from the version - int buildNumber = versionInfo.GetBuildNumber(); - - // Get available packages for this release - List<Package> packageList = await PackagesJsonSchema - .GetPackagesAsync(GetRelease(), buildNumber, cancellationToken) - .ConfigureAwait(false); - - // Get the x64 and arm64 server ubuntu server packages - Package packageX64 = - packageList.Find(item => item.IsX64Server()) - ?? throw new InvalidOperationException( - $"{Product}: No x64 Ubuntu server package found for build {buildNumber} (version {versionInfo.Version})" - ); - if (string.IsNullOrEmpty(packageX64.File)) - { - throw new InvalidOperationException( - $"{Product}: x64 Ubuntu server package for build {buildNumber} (version {versionInfo.Version}) has no file name" - ); - } - Package packageArm64 = - packageList.Find(item => item.IsArm64Server()) - ?? throw new InvalidOperationException( - $"{Product}: No arm64 Ubuntu server package found for build {buildNumber} (version {versionInfo.Version})" - ); - if (string.IsNullOrEmpty(packageArm64.File)) - { - throw new InvalidOperationException( - $"{Product}: arm64 Ubuntu server package for build {buildNumber} (version {versionInfo.Version}) has no file name" - ); - } - - // Create the download URLs - // https://updates.networkoptix.com/{product}/{build}/{file} - versionInfo.UriX64 = - $"https://updates.networkoptix.com/{GetRelease()}/{buildNumber}/{packageX64.File}"; - versionInfo.UriArm64 = - $"https://updates.networkoptix.com/{GetRelease()}/{buildNumber}/{packageArm64.File}"; - - // Verify and add to list - if (VerifyVersion(versionInfo)) - { - Versions.Add(versionInfo); - } - } - - // Make sure all labels are correct - VerifyLabels(); - } - catch (Exception e) when (Log.Logger.LogAndHandle(e)) - { - // Log and rethrow - throw; - } - } - - public VersionInfo CreateVersionInfo(Release release) - { - // Create a version with its label from the release. - // Note: AddLabel() may move the label off other versions already in the list. - if (string.IsNullOrEmpty(release.Version)) - { - throw new InvalidOperationException( - $"{Product}: Release has no version (publication type '{release.PublicationType}')" - ); - } - VersionInfo versionInfo = new(); - versionInfo.SetVersion(release.Version); - AddLabel(versionInfo, release.GetLabel()); - return versionInfo; - } - - private bool VerifyVersion(VersionInfo versionInfo) - { - // Static rules: - - // Ubuntu Noble requires version 6.0 or later - if (versionInfo.CompareTo("6.0") >= 0) - { - return true; - } - - Log.Logger.Warning( - "{Product}:{Version} : Ubuntu Noble requires v6.0+", - Product, - versionInfo.Version - ); - return false; - } - - public void AddLabel(VersionInfo versionInfo, VersionInfo.LabelType label) - { - // Ignore if label is None - if (label == VersionInfo.LabelType.None) - { - return; - } - - // Does this label already exists in other versions - VersionInfo? existingVersion = Versions.Find(item => item.Labels.Contains(label)); - if (existingVersion == null) - { - // New label - versionInfo.Labels.Add(label); - return; - } - - // Is this version larger than the other version - if (versionInfo.CompareTo(existingVersion) <= 0) - { - return; - } - - Log.Logger.Warning( - "{Product}: Replacing {Label} from {ExistingVersion} to {NewVersion}", - Product, - label, - existingVersion.Version, - versionInfo.Version - ); - - // Remove from other version and add to this version - _ = existingVersion.Labels.Remove(label); - versionInfo.Labels.Add(label); - } - - private VersionInfo? FindMissingLabel( - VersionInfo.LabelType targetLabel, - List<VersionInfo.LabelType> sourceLabels - ) - { - foreach (VersionInfo.LabelType label in sourceLabels) - { - // Find last matching item, must be sorted - VersionInfo? version = Versions.FindLast(item => item.Labels.Contains(label)); - if (version == null) - { - continue; - } - - Log.Logger.Warning( - "{Product}: Using {SourceLabel} for {TargetLabel}", - Product, - label, - targetLabel - ); - return version; - } - return null; - } - - public void VerifyLabels() - { - // Sort by version number - Versions.Sort(new VersionInfoComparer()); - - // If no Latest label is set, use Stable or RC or Beta as Latest - if (!Versions.Any(item => item.Labels.Contains(VersionInfo.LabelType.Latest))) - { - VersionInfo latest = - FindMissingLabel( - VersionInfo.LabelType.Latest, - [ - VersionInfo.LabelType.Stable, - VersionInfo.LabelType.RC, - VersionInfo.LabelType.Beta, - ] - ) ?? throw new InvalidOperationException("Latest label could not be resolved."); - latest.Labels.Add(VersionInfo.LabelType.Latest); - } - - // If no Stable label is set, use Latest as stable - if (!Versions.Any(item => item.Labels.Contains(VersionInfo.LabelType.Stable))) - { - VersionInfo stable = - FindMissingLabel(VersionInfo.LabelType.Stable, [VersionInfo.LabelType.Latest]) - ?? throw new InvalidOperationException("Stable label could not be resolved."); - stable.Labels.Add(VersionInfo.LabelType.Stable); - } - - // Remove all versions without labels - _ = Versions.RemoveAll(item => item.Labels.Count == 0); - // Sort by label - Versions.ForEach(item => item.Labels.Sort()); - - // Must have 1 Latest and 1 Stable label - ArgumentOutOfRangeException.ThrowIfNotEqual( - Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)), - 1 - ); - ArgumentOutOfRangeException.ThrowIfNotEqual( - Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)), - 1 - ); - - // Must have no more than 1 Beta or RC labels - ArgumentOutOfRangeException.ThrowIfGreaterThan( - Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)), - 1 - ); - ArgumentOutOfRangeException.ThrowIfGreaterThan( - Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)), - 1 - ); - - // Must have no duplicate version numbers - VerifyNoDuplicateVersions(); - } - - public void VerifyNoDuplicateVersions() - { - // Each version number must appear at most once. - // Duplicates collapse when the matrix builds a set keyed by version number - // (see ImageInfo.CreateImages() and VersionInfoComparer) and must be rejected - // rather than written to the version file. - List<string> duplicateVersions = - [ - .. Versions - .GroupBy(item => VersionInfo.ParseVersion(item.Version)) - .Where(group => group.Count() > 1) - .Select(group => group.First().Version), - ]; - if (duplicateVersions.Count > 0) - { - throw new InvalidOperationException( - $"{Product}: Duplicate version numbers found: {string.Join(", ", duplicateVersions)}" - ); - } - } - - public void LogInformation() - { - foreach (VersionInfo version in Versions) - { - Log.Logger.Information( - "{Product}: Version: {Version}, Label: {Labels}, UriX64: {UriX64}, UriArm64: {UriArm64}", - Product, - version.Version, - version.Labels, - version.UriX64, - version.UriArm64 - ); - } - } - - public async Task VerifyUrlsAsync(CancellationToken cancellationToken) - { - try - { - HttpClient httpClient = HttpClientFactory.GetHttpClient(); - foreach (VersionInfo versionUri in Versions) - { - // Will throw on error - await VerifyUrlAsync(httpClient, new Uri(versionUri.UriX64), cancellationToken) - .ConfigureAwait(false); - await VerifyUrlAsync(httpClient, new Uri(versionUri.UriArm64), cancellationToken) - .ConfigureAwait(false); - } - } - catch (Exception e) when (Log.Logger.LogAndHandle(e)) - { - // Log and rethrow - throw; - } - } - - private static async Task VerifyUrlAsync( - HttpClient httpClient, - Uri url, - CancellationToken cancellationToken - ) - { - // Will throw on failure - ArgumentNullException.ThrowIfNull(httpClient); - ArgumentNullException.ThrowIfNull(url); - - // Get URL - Log.Logger.Information("Verifying Url: {Url}", url); - using HttpResponseMessage httpResponse = await httpClient - .GetAsync(url, cancellationToken) - .ConfigureAwait(false); - _ = httpResponse.EnsureSuccessStatusCode(); - - // Get filename from httpResponse or Uri path - string? fileName = httpResponse.Content.Headers.ContentDisposition?.FileName; - fileName ??= Path.GetFileName(url.LocalPath); - - // Log details - Log.Logger.Information( - "File Name: {FileName}, File Size: {FileSize}, Last Modified: {LastModified}", - fileName, - httpResponse.Content.Headers.ContentLength, - httpResponse.Content.Headers.LastModified - ); - } - - public void Verify() - { - // Match verification logic executed during GetVersions() - - // Verify each version - List<VersionInfo> removeVersions = []; - foreach (VersionInfo version in Versions.Where(version => !VerifyVersion(version))) - { - Log.Logger.Warning("{Product} : Removing {Version}", Product, version.Version); - removeVersions.Add(version); - } - _ = Versions.RemoveAll(removeVersions.Contains); - - // Verify labels - VerifyLabels(); - } -} +namespace CreateMatrix; + +internal sealed class ProductInfo +{ + public enum ProductType + { + None, + NxGo, + NxMeta, + NxWitness, + DWSpectrum, + WisenetWAVE, + } + + public ProductType Product { get; set; } + + public List<VersionInfo> Versions { get; set; } = []; + + public string GetCompany() => GetCompany(Product); + + public string GetRelease() => GetRelease(Product); + + public string GetDescription() => GetDescription(Product); + + public string GetDocker(bool lsio) => GetDocker(Product, lsio); + + // Used for release JSON API https://updates.vmsproxy.com/{release}/releases.json path + public static string GetRelease(ProductType productType) => + productType switch + { + ProductType.NxGo => "nxgo", + ProductType.NxMeta => "metavms", + ProductType.NxWitness => "default", + ProductType.DWSpectrum => "digitalwatchdog", + ProductType.WisenetWAVE => "hanwha", + ProductType.None => throw new InvalidOperationException( + $"{nameof(ProductType)} is None" + ), + _ => throw new InvalidEnumArgumentException( + nameof(productType), + (int)productType, + typeof(ProductType) + ), + }; + + // Used for ${COMPANY_NAME} mediaserver install path and user account + public static string GetCompany(ProductType productType) => + productType switch + { + ProductType.NxGo => "networkoptix", + ProductType.NxMeta => "networkoptix-metavms", + ProductType.NxWitness => "networkoptix", + ProductType.DWSpectrum => "digitalwatchdog", + ProductType.WisenetWAVE => "hanwha", + ProductType.None => throw new InvalidOperationException( + $"{nameof(ProductType)} is None" + ), + _ => throw new InvalidEnumArgumentException( + nameof(productType), + (int)productType, + typeof(ProductType) + ), + }; + + // Used for ${LABEL_DESCRIPTION} in Dockerfile + public static string GetDescription(ProductType productType) => + productType switch + { + ProductType.NxGo => "Nx Go VMS", + ProductType.NxMeta => "Nx Meta VMS", + ProductType.NxWitness => "Nx Witness VMS", + ProductType.DWSpectrum => "DW Spectrum IPVMS", + ProductType.WisenetWAVE => "Wisenet WAVE VMS", + ProductType.None => throw new InvalidOperationException( + $"{nameof(ProductType)} is None" + ), + _ => throw new InvalidEnumArgumentException( + nameof(productType), + (int)productType, + typeof(ProductType) + ), + }; + + // Dockerfile name, excluding the .Dockerfile extension. + // This is the single source of the image/Dockerfile naming convention. ImageInfo derives its + // name from here. The base variant is a bool (Ubuntu vs LSIO). Promote it to an enum only if a + // third base type is ever added (which would also ripple through ComposeFile/DockerFile). + public static string GetDocker(ProductType productType, bool lsio) => + $"{productType}{(lsio ? "-LSIO" : "")}"; + + public static IEnumerable<ProductType> GetProductTypes() => + // Create list of product types + [.. Enum.GetValues<ProductType>().Where(productType => productType != ProductType.None)]; + + public static List<ProductInfo> GetProducts() => + // Create list of all known products + [ + .. from ProductType productType in GetProductTypes() + select new ProductInfo { Product = productType }, + ]; + + public async Task FetchVersionsAsync(CancellationToken cancellationToken) + { + // Get version information using releases.json and package.json + Log.Logger.Information("{Product}: Getting online release information...", Product); + try + { + // Get all releases + List<Release> releasesList = await ReleasesJsonSchema + .GetReleasesAsync(GetRelease(), cancellationToken) + .ConfigureAwait(false); + foreach (Release release in releasesList) + { + // Only process "vms" products + if (!release.Product.Equals(Release.VmsProduct, StringComparison.OrdinalIgnoreCase)) + { + Log.Logger.Warning( + "{Product}: Skipping {ReleaseProduct}", + Product, + release.Product + ); + continue; + } + + // Set the version and label + VersionInfo versionInfo = CreateVersionInfo(release); + + // Get the build number from the version + int buildNumber = versionInfo.GetBuildNumber(); + + // Get available packages for this release + List<Package> packageList = await PackagesJsonSchema + .GetPackagesAsync(GetRelease(), buildNumber, cancellationToken) + .ConfigureAwait(false); + + // Get the x64 and arm64 server ubuntu server packages + Package packageX64 = + packageList.Find(item => item.IsX64Server()) + ?? throw new InvalidOperationException( + $"{Product}: No x64 Ubuntu server package found for build {buildNumber} (version {versionInfo.Version})" + ); + if (string.IsNullOrEmpty(packageX64.File)) + { + throw new InvalidOperationException( + $"{Product}: x64 Ubuntu server package for build {buildNumber} (version {versionInfo.Version}) has no file name" + ); + } + Package packageArm64 = + packageList.Find(item => item.IsArm64Server()) + ?? throw new InvalidOperationException( + $"{Product}: No arm64 Ubuntu server package found for build {buildNumber} (version {versionInfo.Version})" + ); + if (string.IsNullOrEmpty(packageArm64.File)) + { + throw new InvalidOperationException( + $"{Product}: arm64 Ubuntu server package for build {buildNumber} (version {versionInfo.Version}) has no file name" + ); + } + + // Create the download URLs + // https://updates.networkoptix.com/{product}/{build}/{file} + versionInfo.UriX64 = + $"https://updates.networkoptix.com/{GetRelease()}/{buildNumber}/{packageX64.File}"; + versionInfo.UriArm64 = + $"https://updates.networkoptix.com/{GetRelease()}/{buildNumber}/{packageArm64.File}"; + + // Verify and add to list + if (VerifyVersion(versionInfo)) + { + Versions.Add(versionInfo); + } + } + + // Make sure all labels are correct + VerifyLabels(); + } + catch (Exception e) when (Log.Logger.LogAndHandle(e)) + { + // Log and rethrow + throw; + } + } + + public VersionInfo CreateVersionInfo(Release release) + { + // Create a version with its label from the release. + // Note: AddLabel() may move the label off other versions already in the list. + if (string.IsNullOrEmpty(release.Version)) + { + throw new InvalidOperationException( + $"{Product}: Release has no version (publication type '{release.PublicationType}')" + ); + } + VersionInfo versionInfo = new(); + versionInfo.SetVersion(release.Version); + AddLabel(versionInfo, release.GetLabel()); + return versionInfo; + } + + private bool VerifyVersion(VersionInfo versionInfo) + { + // Static rules: + + // Ubuntu Noble requires version 6.0 or later + if (versionInfo.CompareTo("6.0") >= 0) + { + return true; + } + + Log.Logger.Warning( + "{Product}:{Version} : Ubuntu Noble requires v6.0+", + Product, + versionInfo.Version + ); + return false; + } + + public void AddLabel(VersionInfo versionInfo, VersionInfo.LabelType label) + { + // Ignore if label is None + if (label == VersionInfo.LabelType.None) + { + return; + } + + // Does this label already exists in other versions + VersionInfo? existingVersion = Versions.Find(item => item.Labels.Contains(label)); + if (existingVersion == null) + { + // New label + versionInfo.Labels.Add(label); + return; + } + + // Is this version larger than the other version + if (versionInfo.CompareTo(existingVersion) <= 0) + { + return; + } + + Log.Logger.Warning( + "{Product}: Replacing {Label} from {ExistingVersion} to {NewVersion}", + Product, + label, + existingVersion.Version, + versionInfo.Version + ); + + // Remove from other version and add to this version + _ = existingVersion.Labels.Remove(label); + versionInfo.Labels.Add(label); + } + + private VersionInfo? FindMissingLabel( + VersionInfo.LabelType targetLabel, + List<VersionInfo.LabelType> sourceLabels + ) + { + foreach (VersionInfo.LabelType label in sourceLabels) + { + // Find last matching item, must be sorted + VersionInfo? version = Versions.FindLast(item => item.Labels.Contains(label)); + if (version == null) + { + continue; + } + + Log.Logger.Warning( + "{Product}: Using {SourceLabel} for {TargetLabel}", + Product, + label, + targetLabel + ); + return version; + } + return null; + } + + public void VerifyLabels() + { + // Sort by version number + Versions.Sort(new VersionInfoComparer()); + + // If no Latest label is set, use Stable or RC or Beta as Latest + if (!Versions.Any(item => item.Labels.Contains(VersionInfo.LabelType.Latest))) + { + VersionInfo latest = + FindMissingLabel( + VersionInfo.LabelType.Latest, + [ + VersionInfo.LabelType.Stable, + VersionInfo.LabelType.RC, + VersionInfo.LabelType.Beta, + ] + ) ?? throw new InvalidOperationException("Latest label could not be resolved."); + latest.Labels.Add(VersionInfo.LabelType.Latest); + } + + // If no Stable label is set, use Latest as stable + if (!Versions.Any(item => item.Labels.Contains(VersionInfo.LabelType.Stable))) + { + VersionInfo stable = + FindMissingLabel(VersionInfo.LabelType.Stable, [VersionInfo.LabelType.Latest]) + ?? throw new InvalidOperationException("Stable label could not be resolved."); + stable.Labels.Add(VersionInfo.LabelType.Stable); + } + + // Remove all versions without labels + _ = Versions.RemoveAll(item => item.Labels.Count == 0); + // Sort by label + Versions.ForEach(item => item.Labels.Sort()); + + // Must have 1 Latest and 1 Stable label + ArgumentOutOfRangeException.ThrowIfNotEqual( + Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)), + 1 + ); + ArgumentOutOfRangeException.ThrowIfNotEqual( + Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)), + 1 + ); + + // Must have no more than 1 Beta or RC labels + ArgumentOutOfRangeException.ThrowIfGreaterThan( + Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)), + 1 + ); + ArgumentOutOfRangeException.ThrowIfGreaterThan( + Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)), + 1 + ); + + // Must have no duplicate version numbers + VerifyNoDuplicateVersions(); + } + + public void VerifyNoDuplicateVersions() + { + // Each version number must appear at most once. + // Duplicates collapse when the matrix builds a set keyed by version number + // (see ImageInfo.CreateImages() and VersionInfoComparer) and must be rejected + // rather than written to the version file. + List<string> duplicateVersions = + [ + .. Versions + .GroupBy(item => VersionInfo.ParseVersion(item.Version)) + .Where(group => group.Count() > 1) + .Select(group => group.First().Version), + ]; + if (duplicateVersions.Count > 0) + { + throw new InvalidOperationException( + $"{Product}: Duplicate version numbers found: {string.Join(", ", duplicateVersions)}" + ); + } + } + + public void LogInformation() + { + foreach (VersionInfo version in Versions) + { + Log.Logger.Information( + "{Product}: Version: {Version}, Label: {Labels}, UriX64: {UriX64}, UriArm64: {UriArm64}", + Product, + version.Version, + version.Labels, + version.UriX64, + version.UriArm64 + ); + } + } + + public async Task VerifyUrlsAsync(CancellationToken cancellationToken) + { + try + { + HttpClient httpClient = HttpClientFactory.GetHttpClient(); + foreach (VersionInfo versionUri in Versions) + { + // Will throw on error + await VerifyUrlAsync(httpClient, new Uri(versionUri.UriX64), cancellationToken) + .ConfigureAwait(false); + await VerifyUrlAsync(httpClient, new Uri(versionUri.UriArm64), cancellationToken) + .ConfigureAwait(false); + } + } + catch (Exception e) when (Log.Logger.LogAndHandle(e)) + { + // Log and rethrow + throw; + } + } + + private static async Task VerifyUrlAsync( + HttpClient httpClient, + Uri url, + CancellationToken cancellationToken + ) + { + // Will throw on failure + ArgumentNullException.ThrowIfNull(httpClient); + ArgumentNullException.ThrowIfNull(url); + + // Get URL + Log.Logger.Information("Verifying Url: {Url}", url); + using HttpResponseMessage httpResponse = await httpClient + .GetAsync(url, cancellationToken) + .ConfigureAwait(false); + _ = httpResponse.EnsureSuccessStatusCode(); + + // Get filename from httpResponse or Uri path + string? fileName = httpResponse.Content.Headers.ContentDisposition?.FileName; + fileName ??= Path.GetFileName(url.LocalPath); + + // Log details + Log.Logger.Information( + "File Name: {FileName}, File Size: {FileSize}, Last Modified: {LastModified}", + fileName, + httpResponse.Content.Headers.ContentLength, + httpResponse.Content.Headers.LastModified + ); + } + + public void Verify() + { + // Match verification logic executed during GetVersions() + + // Verify each version + List<VersionInfo> removeVersions = []; + foreach (VersionInfo version in Versions.Where(version => !VerifyVersion(version))) + { + Log.Logger.Warning("{Product} : Removing {Version}", Product, version.Version); + removeVersions.Add(version); + } + _ = Versions.RemoveAll(removeVersions.Contains); + + // Verify labels + VerifyLabels(); + } +} diff --git a/CreateMatrix/Program.cs b/CreateMatrix/Program.cs index 770f72b8..0973d468 100644 --- a/CreateMatrix/Program.cs +++ b/CreateMatrix/Program.cs @@ -1,188 +1,188 @@ -using Serilog.Sinks.SystemConsole.Themes; - -// dotnet publish --self-contained false --output ./publish -// ./publish/CreateMatrix matrix --updateversion --matrixpath ./JSON/Matrix.json --versionpath ./JSON/Version.json -// echo $? - -namespace CreateMatrix; - -internal sealed class Program( - CommandLine.Options commandLineOptions, - CancellationToken cancellationToken -) -{ - internal static async Task<int> Main(string[] args) - { - try - { - // Parse commandline - CommandLine commandLine = new(args); - - // Bypass startup for errors or help and version commands - if (CommandLine.BypassStartup(commandLine.Result)) - { - return await commandLine.Result.InvokeAsync().ConfigureAwait(false); - } - - // Log to the console - LoggerConfiguration loggerConfiguration = new LoggerConfiguration() - .Enrich.WithThreadId() - .WriteTo.Console( - theme: AnsiConsoleTheme.Code, - formatProvider: CultureInfo.InvariantCulture, - outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [t:{ThreadId}{ThreadName}] {Message:lj}{NewLine}{Exception}" - ); - Log.Logger = loggerConfiguration.CreateLogger(); - - // Invoke command - return await commandLine.Result.InvokeAsync().ConfigureAwait(false); - } - catch (OperationCanceledException) - { - Log.Logger.Warning("Operation was cancelled."); - return 130; // POSIX standard for SIGINT - } - catch (Exception ex) when (Log.Logger.LogAndHandle(ex)) - { - return 1; - } - finally - { - await Log.CloseAndFlushAsync().ConfigureAwait(false); - } - } - - internal async Task<int> ExecuteVersionAsync() - { - ArgumentNullException.ThrowIfNull(commandLineOptions.VersionPath); - - // Get versions for all products using releases API - VersionJsonSchema versionSchema = new(); - Log.Logger.Information("Getting version information online..."); - versionSchema.Products.AddRange(ProductInfo.GetProducts()); - foreach (ProductInfo productInfo in versionSchema.Products) - { - await productInfo.FetchVersionsAsync(cancellationToken).ConfigureAwait(false); - productInfo.LogInformation(); - await productInfo.VerifyUrlsAsync(cancellationToken).ConfigureAwait(false); - } - - // Write to file - Log.Logger.Information( - "Writing version information to {Path}", - commandLineOptions.VersionPath.FullName - ); - VersionJsonSchema.ToFile(commandLineOptions.VersionPath.FullName, versionSchema); - - return 0; - } - - internal async Task<int> ExecuteMatrixAsync() - { - ArgumentNullException.ThrowIfNull(commandLineOptions.VersionPath); - ArgumentNullException.ThrowIfNull(commandLineOptions.MatrixPath); - - // Load version info from file - Log.Logger.Information( - "Reading version information from {Path}", - commandLineOptions.VersionPath.FullName - ); - VersionJsonSchema fileSchema = VersionJsonSchema.FromFile( - commandLineOptions.VersionPath.FullName - ); - - // Re-verify as rules may have changed after file was written - foreach (ProductInfo productInfo in fileSchema.Products) - { - productInfo.Verify(); - productInfo.LogInformation(); - } - - // Update version information - if (commandLineOptions.UpdateVersion) - { - // Get versions for all products using releases API - VersionJsonSchema onlineSchema = new(); - Log.Logger.Information("Getting version information online..."); - onlineSchema.Products.AddRange(ProductInfo.GetProducts()); - foreach (ProductInfo productInfo in onlineSchema.Products) - { - await productInfo.FetchVersionsAsync(cancellationToken).ConfigureAwait(false); - productInfo.LogInformation(); - } - - // Make sure the labelled version numbers do not regress - ReleaseVersionForward.Verify(fileSchema.Products, onlineSchema.Products); - - // Make sure the forward merge did not introduce duplicate version numbers - onlineSchema.Products.ForEach(productInfo => productInfo.VerifyNoDuplicateVersions()); - - // Verify URL's - foreach (ProductInfo productInfo in onlineSchema.Products) - { - await productInfo.VerifyUrlsAsync(cancellationToken).ConfigureAwait(false); - } - - // Update the file version with the online version - Log.Logger.Information( - "Writing version information to {Path}", - commandLineOptions.VersionPath.FullName - ); - VersionJsonSchema.ToFile(commandLineOptions.VersionPath.FullName, onlineSchema); - fileSchema = onlineSchema; - } - else - { - // Verify URL's - foreach (ProductInfo productInfo in fileSchema.Products) - { - await productInfo.VerifyUrlsAsync(cancellationToken).ConfigureAwait(false); - } - } - - // Create matrix - Log.Logger.Information("Creating Matrix from versions"); - MatrixJsonSchema matrixSchema = new(); - matrixSchema.Images.AddRange(ImageInfo.CreateImages(fileSchema.Products)); - Log.Logger.Information("Created {Count} images in matrix", matrixSchema.Images.Count); - matrixSchema.Images.ForEach(imageInfo => imageInfo.LogInformation()); - - // Write matrix - Log.Logger.Information( - "Writing matrix information to {Path}", - commandLineOptions.MatrixPath.FullName - ); - MatrixJsonSchema.ToFile(commandLineOptions.MatrixPath.FullName, matrixSchema); - - return 0; - } - - internal Task<int> ExecuteMakeAsync() - { - ArgumentNullException.ThrowIfNull(commandLineOptions.VersionPath); - ArgumentNullException.ThrowIfNull(commandLineOptions.MakeDirectory); - ArgumentNullException.ThrowIfNull(commandLineOptions.DockerDirectory); - - // Load version info from file - Log.Logger.Information( - "Reading version information from {Path}", - commandLineOptions.VersionPath.FullName - ); - VersionJsonSchema versionSchema = VersionJsonSchema.FromFile( - commandLineOptions.VersionPath.FullName - ); - - // Create Compose files - ComposeFile.Create(commandLineOptions.MakeDirectory); - - // Create Docker files - List<ProductInfo> products = [.. versionSchema.Products]; - DockerFile.Create( - products, - commandLineOptions.DockerDirectory.FullName, - commandLineOptions.VersionLabel - ); - - return Task.FromResult(0); - } -} +using Serilog.Sinks.SystemConsole.Themes; + +// dotnet publish --self-contained false --output ./publish +// ./publish/CreateMatrix matrix --updateversion --matrixpath ./JSON/Matrix.json --versionpath ./JSON/Version.json +// echo $? + +namespace CreateMatrix; + +internal sealed class Program( + CommandLine.Options commandLineOptions, + CancellationToken cancellationToken +) +{ + internal static async Task<int> Main(string[] args) + { + try + { + // Parse commandline + CommandLine commandLine = new(args); + + // Bypass startup for errors or help and version commands + if (CommandLine.BypassStartup(commandLine.Result)) + { + return await commandLine.Result.InvokeAsync().ConfigureAwait(false); + } + + // Log to the console + LoggerConfiguration loggerConfiguration = new LoggerConfiguration() + .Enrich.WithThreadId() + .WriteTo.Console( + theme: AnsiConsoleTheme.Code, + formatProvider: CultureInfo.InvariantCulture, + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [t:{ThreadId}{ThreadName}] {Message:lj}{NewLine}{Exception}" + ); + Log.Logger = loggerConfiguration.CreateLogger(); + + // Invoke command + return await commandLine.Result.InvokeAsync().ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Log.Logger.Warning("Operation was cancelled."); + return 130; // POSIX standard for SIGINT + } + catch (Exception ex) when (Log.Logger.LogAndHandle(ex)) + { + return 1; + } + finally + { + await Log.CloseAndFlushAsync().ConfigureAwait(false); + } + } + + internal async Task<int> ExecuteVersionAsync() + { + ArgumentNullException.ThrowIfNull(commandLineOptions.VersionPath); + + // Get versions for all products using releases API + VersionJsonSchema versionSchema = new(); + Log.Logger.Information("Getting version information online..."); + versionSchema.Products.AddRange(ProductInfo.GetProducts()); + foreach (ProductInfo productInfo in versionSchema.Products) + { + await productInfo.FetchVersionsAsync(cancellationToken).ConfigureAwait(false); + productInfo.LogInformation(); + await productInfo.VerifyUrlsAsync(cancellationToken).ConfigureAwait(false); + } + + // Write to file + Log.Logger.Information( + "Writing version information to {Path}", + commandLineOptions.VersionPath.FullName + ); + VersionJsonSchema.ToFile(commandLineOptions.VersionPath.FullName, versionSchema); + + return 0; + } + + internal async Task<int> ExecuteMatrixAsync() + { + ArgumentNullException.ThrowIfNull(commandLineOptions.VersionPath); + ArgumentNullException.ThrowIfNull(commandLineOptions.MatrixPath); + + // Load version info from file + Log.Logger.Information( + "Reading version information from {Path}", + commandLineOptions.VersionPath.FullName + ); + VersionJsonSchema fileSchema = VersionJsonSchema.FromFile( + commandLineOptions.VersionPath.FullName + ); + + // Re-verify as rules may have changed after file was written + foreach (ProductInfo productInfo in fileSchema.Products) + { + productInfo.Verify(); + productInfo.LogInformation(); + } + + // Update version information + if (commandLineOptions.UpdateVersion) + { + // Get versions for all products using releases API + VersionJsonSchema onlineSchema = new(); + Log.Logger.Information("Getting version information online..."); + onlineSchema.Products.AddRange(ProductInfo.GetProducts()); + foreach (ProductInfo productInfo in onlineSchema.Products) + { + await productInfo.FetchVersionsAsync(cancellationToken).ConfigureAwait(false); + productInfo.LogInformation(); + } + + // Make sure the labelled version numbers do not regress + ReleaseVersionForward.Verify(fileSchema.Products, onlineSchema.Products); + + // Make sure the forward merge did not introduce duplicate version numbers + onlineSchema.Products.ForEach(productInfo => productInfo.VerifyNoDuplicateVersions()); + + // Verify URL's + foreach (ProductInfo productInfo in onlineSchema.Products) + { + await productInfo.VerifyUrlsAsync(cancellationToken).ConfigureAwait(false); + } + + // Update the file version with the online version + Log.Logger.Information( + "Writing version information to {Path}", + commandLineOptions.VersionPath.FullName + ); + VersionJsonSchema.ToFile(commandLineOptions.VersionPath.FullName, onlineSchema); + fileSchema = onlineSchema; + } + else + { + // Verify URL's + foreach (ProductInfo productInfo in fileSchema.Products) + { + await productInfo.VerifyUrlsAsync(cancellationToken).ConfigureAwait(false); + } + } + + // Create matrix + Log.Logger.Information("Creating Matrix from versions"); + MatrixJsonSchema matrixSchema = new(); + matrixSchema.Images.AddRange(ImageInfo.CreateImages(fileSchema.Products)); + Log.Logger.Information("Created {Count} images in matrix", matrixSchema.Images.Count); + matrixSchema.Images.ForEach(imageInfo => imageInfo.LogInformation()); + + // Write matrix + Log.Logger.Information( + "Writing matrix information to {Path}", + commandLineOptions.MatrixPath.FullName + ); + MatrixJsonSchema.ToFile(commandLineOptions.MatrixPath.FullName, matrixSchema); + + return 0; + } + + internal Task<int> ExecuteMakeAsync() + { + ArgumentNullException.ThrowIfNull(commandLineOptions.VersionPath); + ArgumentNullException.ThrowIfNull(commandLineOptions.MakeDirectory); + ArgumentNullException.ThrowIfNull(commandLineOptions.DockerDirectory); + + // Load version info from file + Log.Logger.Information( + "Reading version information from {Path}", + commandLineOptions.VersionPath.FullName + ); + VersionJsonSchema versionSchema = VersionJsonSchema.FromFile( + commandLineOptions.VersionPath.FullName + ); + + // Create Compose files + ComposeFile.Create(commandLineOptions.MakeDirectory); + + // Create Docker files + List<ProductInfo> products = [.. versionSchema.Products]; + DockerFile.Create( + products, + commandLineOptions.DockerDirectory.FullName, + commandLineOptions.VersionLabel + ); + + return Task.FromResult(0); + } +} diff --git a/CreateMatrix/Properties/launchSettings.json b/CreateMatrix/Properties/launchSettings.json index 04b34339..57b77928 100644 --- a/CreateMatrix/Properties/launchSettings.json +++ b/CreateMatrix/Properties/launchSettings.json @@ -1,28 +1,28 @@ -{ - "profiles": { - "WSL": { - "commandName": "WSL2", - "distributionName": "" - }, - "Matrix": { - "commandName": "Project", - "commandLineArgs": "matrix --versionpath=./Make/Version.json --matrixpath=./Make/Matrix.json --updateversion", - "workingDirectory": "$(SolutionDir)" - }, - "Version": { - "commandName": "Project", - "commandLineArgs": "version --versionpath=./Make/Version.json", - "workingDirectory": "$(SolutionDir)" - }, - "Schema": { - "commandName": "Project", - "commandLineArgs": "schema --versionschemapath=./JSON/Version.schema.json --matrixschemapath=./JSON/Matrix.schema.json", - "workingDirectory": "$(ProjectDir)" - }, - "Make": { - "commandName": "Project", - "commandLineArgs": "make --versionpath=./Make/Version.json --makedirectory=./Make --dockerdirectory=./Docker --versionlabel=Beta", - "workingDirectory": "$(SolutionDir)" - } - } -} +{ + "profiles": { + "WSL": { + "commandName": "WSL2", + "distributionName": "" + }, + "Matrix": { + "commandName": "Project", + "commandLineArgs": "matrix --versionpath=./Make/Version.json --matrixpath=./Make/Matrix.json --updateversion", + "workingDirectory": "$(SolutionDir)" + }, + "Version": { + "commandName": "Project", + "commandLineArgs": "version --versionpath=./Make/Version.json", + "workingDirectory": "$(SolutionDir)" + }, + "Schema": { + "commandName": "Project", + "commandLineArgs": "schema --versionschemapath=./JSON/Version.schema.json --matrixschemapath=./JSON/Matrix.schema.json", + "workingDirectory": "$(ProjectDir)" + }, + "Make": { + "commandName": "Project", + "commandLineArgs": "make --versionpath=./Make/Version.json --makedirectory=./Make --dockerdirectory=./Docker --versionlabel=Beta", + "workingDirectory": "$(SolutionDir)" + } + } +} diff --git a/CreateMatrix/ReleaseVersionForward.cs b/CreateMatrix/ReleaseVersionForward.cs index 14e0fcd4..c593759f 100644 --- a/CreateMatrix/ReleaseVersionForward.cs +++ b/CreateMatrix/ReleaseVersionForward.cs @@ -1,138 +1,138 @@ -namespace CreateMatrix; - -internal static class ReleaseVersionForward -{ - public static void Verify(List<ProductInfo> oldProductList, List<ProductInfo> newProductList) - { - // newProductList will be updated in-place - - // Verify against all products in the old list - foreach (ProductInfo oldProduct in oldProductList) - { - // Find matching new product, must be present - ProductInfo newProduct = newProductList.First(item => - item.Product == oldProduct.Product - ); - - // Verify all labels - foreach (VersionInfo.LabelType label in VersionInfo.GetLabelTypes()) - { - Verify(oldProduct, newProduct, label); - } - } - } - - private static void Verify( - ProductInfo oldProduct, - ProductInfo newProduct, - VersionInfo.LabelType label - ) - { - // NOTE: Forward-only is intentional. A published tag must not regress to a lesser version. - // If a label is released, then pulled, then re-released with a lesser version, we keep the - // old (higher) version. This is harmless while the old build is still downloadable, and if - // its files were actually removed the run fails loudly in VerifyUrlsAsync (404) for a human - // to resolve, e.g. by manually adjusting Version.json. There is no silent-corruption path. - - // Find label in old and new product, skip if not present - VersionInfo? oldVersion = oldProduct.Versions.Find(item => item.Labels.Contains(label)); - if (oldVersion == null) - { - Log.Logger.Warning( - "{Product}:{Label} : Label not found in old versions", - oldProduct.Product, - label - ); - return; - } - - // Find label in new product, skip if not present - VersionInfo? newVersion = newProduct.Versions.Find(item => item.Labels.Contains(label)); - if (newVersion == null) - { - Log.Logger.Warning( - "{Product}:{Label} : Label not found in new versions", - newProduct.Product, - label - ); - return; - } - - // New version must be >= old version - if (oldVersion.CompareTo(newVersion) <= 0) - { - return; - } - - Log.Logger.Error( - "{Product}:{Label} : OldVersion: {OldVersion} > NewVersion: {NewVersion}", - newProduct.Product, - label, - oldVersion.Version, - newVersion.Version - ); - - // Do all the labels match - if (oldVersion.Labels.SequenceEqual(newVersion.Labels)) - { - Log.Logger.Warning( - "{Product}:{Label} Using OldVersion: {OldVersion} instead of NewVersion: {NewVersion}", - newProduct.Product, - label, - oldVersion.Version, - newVersion.Version - ); - - // Remove the regressed new version - _ = newProduct.Versions.Remove(newVersion); - - // The old version number may already be present in the new list under a different - // label (e.g. restoring Latest onto a version that is already Stable). Fold the - // old version's labels into that existing entry instead of adding a duplicate row, - // otherwise two entries would share a version number. - VersionInfo? existingVersion = newProduct.Versions.Find(item => - item.CompareTo(oldVersion) == 0 - ); - if (existingVersion == null) - { - // No existing entry, add the old version - newProduct.Versions.Add(oldVersion); - } - else - { - // Fold the old version's labels into the existing entry - Log.Logger.Warning( - "{Product}:{Label} Folding OldVersion: {OldVersion} labels into existing version", - newProduct.Product, - label, - oldVersion.Version - ); - foreach (VersionInfo.LabelType oldLabel in oldVersion.Labels) - { - if (!existingVersion.Labels.Contains(oldLabel)) - { - existingVersion.Labels.Add(oldLabel); - } - } - existingVersion.Labels.Sort(); - } - } - else - { - // The label moved between versions that carry different label sets, so a surgical - // per-version-label swap is ambiguous. Rather than attempting to unwind individual - // version-label pairs, take the conservative approach and revert the whole product to - // the last-known-good versions. This may discard newer online versions until the - // regression clears, but it avoids producing an inconsistent label arrangement. - Log.Logger.Warning( - "{Product}:{Label} : Labels differ, reverting all versions to old versions", - newProduct.Product, - label - ); - - // Replace all versions if the labels do not match - newProduct.Versions.Clear(); - newProduct.Versions.AddRange(oldProduct.Versions); - } - } -} +namespace CreateMatrix; + +internal static class ReleaseVersionForward +{ + public static void Verify(List<ProductInfo> oldProductList, List<ProductInfo> newProductList) + { + // newProductList will be updated in-place + + // Verify against all products in the old list + foreach (ProductInfo oldProduct in oldProductList) + { + // Find matching new product, must be present + ProductInfo newProduct = newProductList.First(item => + item.Product == oldProduct.Product + ); + + // Verify all labels + foreach (VersionInfo.LabelType label in VersionInfo.GetLabelTypes()) + { + Verify(oldProduct, newProduct, label); + } + } + } + + private static void Verify( + ProductInfo oldProduct, + ProductInfo newProduct, + VersionInfo.LabelType label + ) + { + // NOTE: Forward-only is intentional. A published tag must not regress to a lesser version. + // If a label is released, then pulled, then re-released with a lesser version, we keep the + // old (higher) version. This is harmless while the old build is still downloadable, and if + // its files were actually removed the run fails loudly in VerifyUrlsAsync (404) for a human + // to resolve, e.g. by manually adjusting Version.json. There is no silent-corruption path. + + // Find label in old and new product, skip if not present + VersionInfo? oldVersion = oldProduct.Versions.Find(item => item.Labels.Contains(label)); + if (oldVersion == null) + { + Log.Logger.Warning( + "{Product}:{Label} : Label not found in old versions", + oldProduct.Product, + label + ); + return; + } + + // Find label in new product, skip if not present + VersionInfo? newVersion = newProduct.Versions.Find(item => item.Labels.Contains(label)); + if (newVersion == null) + { + Log.Logger.Warning( + "{Product}:{Label} : Label not found in new versions", + newProduct.Product, + label + ); + return; + } + + // New version must be >= old version + if (oldVersion.CompareTo(newVersion) <= 0) + { + return; + } + + Log.Logger.Error( + "{Product}:{Label} : OldVersion: {OldVersion} > NewVersion: {NewVersion}", + newProduct.Product, + label, + oldVersion.Version, + newVersion.Version + ); + + // Do all the labels match + if (oldVersion.Labels.SequenceEqual(newVersion.Labels)) + { + Log.Logger.Warning( + "{Product}:{Label} Using OldVersion: {OldVersion} instead of NewVersion: {NewVersion}", + newProduct.Product, + label, + oldVersion.Version, + newVersion.Version + ); + + // Remove the regressed new version + _ = newProduct.Versions.Remove(newVersion); + + // The old version number may already be present in the new list under a different + // label (e.g. restoring Latest onto a version that is already Stable). Fold the + // old version's labels into that existing entry instead of adding a duplicate row, + // otherwise two entries would share a version number. + VersionInfo? existingVersion = newProduct.Versions.Find(item => + item.CompareTo(oldVersion) == 0 + ); + if (existingVersion == null) + { + // No existing entry, add the old version + newProduct.Versions.Add(oldVersion); + } + else + { + // Fold the old version's labels into the existing entry + Log.Logger.Warning( + "{Product}:{Label} Folding OldVersion: {OldVersion} labels into existing version", + newProduct.Product, + label, + oldVersion.Version + ); + foreach (VersionInfo.LabelType oldLabel in oldVersion.Labels) + { + if (!existingVersion.Labels.Contains(oldLabel)) + { + existingVersion.Labels.Add(oldLabel); + } + } + existingVersion.Labels.Sort(); + } + } + else + { + // The label moved between versions that carry different label sets, so a surgical + // per-version-label swap is ambiguous. Rather than attempting to unwind individual + // version-label pairs, take the conservative approach and revert the whole product to + // the last-known-good versions. This may discard newer online versions until the + // regression clears, but it avoids producing an inconsistent label arrangement. + Log.Logger.Warning( + "{Product}:{Label} : Labels differ, reverting all versions to old versions", + newProduct.Product, + label + ); + + // Replace all versions if the labels do not match + newProduct.Versions.Clear(); + newProduct.Versions.AddRange(oldProduct.Versions); + } + } +} diff --git a/CreateMatrix/ReleasesJsonSchema.cs b/CreateMatrix/ReleasesJsonSchema.cs index d44909ca..e398dac1 100644 --- a/CreateMatrix/ReleasesJsonSchema.cs +++ b/CreateMatrix/ReleasesJsonSchema.cs @@ -1,159 +1,159 @@ -namespace CreateMatrix; - -// https://updates.vmsproxy.com/{release}/releases.json -// https://updates.vmsproxy.com/default/releases.json -// https://updates.vmsproxy.com/metavms/releases.json -// https://updates.vmsproxy.com/digitalwatchdog/releases.json -// https://updates.vmsproxy.com/hanwha/releases.json - -internal class Release -{ - [JsonPropertyName("product")] - public string Product { get; set; } = string.Empty; - - [JsonPropertyName("version")] - public string Version { get; set; } = string.Empty; - - [JsonPropertyName("publication_type")] - public string PublicationType { get; set; } = string.Empty; - - [JsonPropertyName("release_date")] - public long? ReleaseDate { get; set; } - - [JsonPropertyName("release_delivery_days")] - public long? ReleaseDeliveryDays { get; set; } - - public VersionInfo.LabelType GetLabel() => - // Determine the equivalent label - PublicationType switch - { - // Use Stable or Latest based on if published or not - ReleasePublication => IsPublished() - ? VersionInfo.LabelType.Stable - : VersionInfo.LabelType.Latest, - RcPublication => VersionInfo.LabelType.RC, - BetaPublication => VersionInfo.LabelType.Beta, - _ => throw new InvalidOperationException( - $"Unknown publication type '{PublicationType}' for version {Version}" - ), - }; - - public const string ReleasePublication = "release"; - public const string RcPublication = "rc"; - public const string BetaPublication = "beta"; - public const string VmsProduct = "vms"; - public const string DesktopProduct = "desktop_client"; - - private bool IsPublished() => - // Logic follows similar patterns as used in C++ Desktop Client - // https://github.com/networkoptix/nx_open/blob/526967920636d3119c92a5220290ecc10957bf12/vms/libs/nx_vms_update/src/nx/vms/update/releases_info.cpp#L57 - // releases_info.cpp: ReleasesInfo::selectVmsRelease(), isBuildPublished(), canReceiveUnpublishedBuild() - ReleaseDate > 0 - && ReleaseDeliveryDays >= 0; -} - -internal class ReleasesJsonSchema -{ - [JsonPropertyName("releases")] - public List<Release> Releases { get; set; } = []; - - private static ReleasesJsonSchema FromJson(string json) - { - ReleasesJsonSchema? jsonSchema = JsonSerializer.Deserialize( - json, - ReleasesJsonContext.Default.ReleasesJsonSchema - ); - ArgumentNullException.ThrowIfNull(jsonSchema); - return jsonSchema; - } - - public static async Task<List<Release>> GetReleasesAsync( - string productName, - CancellationToken cancellationToken - ) - { - // Load releases JSON - // https://updates.vmsproxy.com/{product}/releases.json - ArgumentNullException.ThrowIfNull(productName); - - HttpClient httpClient = HttpClientFactory.GetHttpClient(); - Uri releasesUri = new($"https://updates.vmsproxy.com/{productName}/releases.json"); - Log.Logger.Information("Getting release information from {Uri}", releasesUri); - string jsonString = await httpClient - .GetStringAsync(releasesUri, cancellationToken) - .ConfigureAwait(false); - - // Deserialize JSON - ReleasesJsonSchema releasesSchema = FromJson(jsonString); - ArgumentNullException.ThrowIfNull(releasesSchema); - if (releasesSchema.Releases.Count == 0) - { - throw new InvalidOperationException($"No releases found in {releasesUri}"); - } - - // Verify the vendor data and return the folded releases - return VerifyReleases(releasesSchema.Releases); - } - - public static List<Release> VerifyReleases(List<Release> releases) - { - // A version number must carry a single, unambiguous publication type. - // Fold benign duplicates (same version and publication type), reject conflicts. - ArgumentNullException.ThrowIfNull(releases); - - // A version number must be present and parseable, otherwise grouping below would - // throw a context-free FormatException from Version parsing. - foreach (Release release in releases) - { - if ( - string.IsNullOrEmpty(release.Version) - || !Version.TryParse(VersionInfo.NormalizeVersion(release.Version), out _) - ) - { - throw new InvalidOperationException( - $"{release.Product}: Invalid or missing version '{release.Version}' (publication type '{release.PublicationType}')" - ); - } - } - - List<Release> verifiedReleases = []; - foreach ( - IGrouping<(string Product, Version Version), Release> group in releases.GroupBy( - release => (release.Product, VersionInfo.ParseVersion(release.Version)) - ) - ) - { - // A version must not be tagged with more than one publication type - List<string> publicationTypes = - [ - .. group - .Select(release => release.PublicationType) - .Distinct(StringComparer.OrdinalIgnoreCase), - ]; - if (publicationTypes.Count > 1) - { - throw new InvalidOperationException( - $"{group.Key.Product}: Version {group.First().Version} has conflicting publication types: {string.Join(", ", publicationTypes)}" - ); - } - - // Keep the first entry, folding any same-version same-type duplicates - verifiedReleases.Add(group.First()); - } - return verifiedReleases; - } -} - -[JsonSourceGenerationOptions( - AllowTrailingCommas = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - IncludeFields = true, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, - ReadCommentHandling = JsonCommentHandling.Skip, - UseStringEnumConverter = true, - WriteIndented = true, - NewLine = "\r\n" -)] -[JsonSerializable(typeof(ReleasesJsonSchema))] -internal partial class ReleasesJsonContext : JsonSerializerContext; +namespace CreateMatrix; + +// https://updates.vmsproxy.com/{release}/releases.json +// https://updates.vmsproxy.com/default/releases.json +// https://updates.vmsproxy.com/metavms/releases.json +// https://updates.vmsproxy.com/digitalwatchdog/releases.json +// https://updates.vmsproxy.com/hanwha/releases.json + +internal class Release +{ + [JsonPropertyName("product")] + public string Product { get; set; } = string.Empty; + + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; + + [JsonPropertyName("publication_type")] + public string PublicationType { get; set; } = string.Empty; + + [JsonPropertyName("release_date")] + public long? ReleaseDate { get; set; } + + [JsonPropertyName("release_delivery_days")] + public long? ReleaseDeliveryDays { get; set; } + + public VersionInfo.LabelType GetLabel() => + // Determine the equivalent label + PublicationType switch + { + // Use Stable or Latest based on if published or not + ReleasePublication => IsPublished() + ? VersionInfo.LabelType.Stable + : VersionInfo.LabelType.Latest, + RcPublication => VersionInfo.LabelType.RC, + BetaPublication => VersionInfo.LabelType.Beta, + _ => throw new InvalidOperationException( + $"Unknown publication type '{PublicationType}' for version {Version}" + ), + }; + + public const string ReleasePublication = "release"; + public const string RcPublication = "rc"; + public const string BetaPublication = "beta"; + public const string VmsProduct = "vms"; + public const string DesktopProduct = "desktop_client"; + + private bool IsPublished() => + // Logic follows similar patterns as used in C++ Desktop Client + // https://github.com/networkoptix/nx_open/blob/526967920636d3119c92a5220290ecc10957bf12/vms/libs/nx_vms_update/src/nx/vms/update/releases_info.cpp#L57 + // releases_info.cpp: ReleasesInfo::selectVmsRelease(), isBuildPublished(), canReceiveUnpublishedBuild() + ReleaseDate > 0 + && ReleaseDeliveryDays >= 0; +} + +internal class ReleasesJsonSchema +{ + [JsonPropertyName("releases")] + public List<Release> Releases { get; set; } = []; + + private static ReleasesJsonSchema FromJson(string json) + { + ReleasesJsonSchema? jsonSchema = JsonSerializer.Deserialize( + json, + ReleasesJsonContext.Default.ReleasesJsonSchema + ); + ArgumentNullException.ThrowIfNull(jsonSchema); + return jsonSchema; + } + + public static async Task<List<Release>> GetReleasesAsync( + string productName, + CancellationToken cancellationToken + ) + { + // Load releases JSON + // https://updates.vmsproxy.com/{product}/releases.json + ArgumentNullException.ThrowIfNull(productName); + + HttpClient httpClient = HttpClientFactory.GetHttpClient(); + Uri releasesUri = new($"https://updates.vmsproxy.com/{productName}/releases.json"); + Log.Logger.Information("Getting release information from {Uri}", releasesUri); + string jsonString = await httpClient + .GetStringAsync(releasesUri, cancellationToken) + .ConfigureAwait(false); + + // Deserialize JSON + ReleasesJsonSchema releasesSchema = FromJson(jsonString); + ArgumentNullException.ThrowIfNull(releasesSchema); + if (releasesSchema.Releases.Count == 0) + { + throw new InvalidOperationException($"No releases found in {releasesUri}"); + } + + // Verify the vendor data and return the folded releases + return VerifyReleases(releasesSchema.Releases); + } + + public static List<Release> VerifyReleases(List<Release> releases) + { + // A version number must carry a single, unambiguous publication type. + // Fold benign duplicates (same version and publication type), reject conflicts. + ArgumentNullException.ThrowIfNull(releases); + + // A version number must be present and parseable, otherwise grouping below would + // throw a context-free FormatException from Version parsing. + foreach (Release release in releases) + { + if ( + string.IsNullOrEmpty(release.Version) + || !Version.TryParse(VersionInfo.NormalizeVersion(release.Version), out _) + ) + { + throw new InvalidOperationException( + $"{release.Product}: Invalid or missing version '{release.Version}' (publication type '{release.PublicationType}')" + ); + } + } + + List<Release> verifiedReleases = []; + foreach ( + IGrouping<(string Product, Version Version), Release> group in releases.GroupBy( + release => (release.Product, VersionInfo.ParseVersion(release.Version)) + ) + ) + { + // A version must not be tagged with more than one publication type + List<string> publicationTypes = + [ + .. group + .Select(release => release.PublicationType) + .Distinct(StringComparer.OrdinalIgnoreCase), + ]; + if (publicationTypes.Count > 1) + { + throw new InvalidOperationException( + $"{group.Key.Product}: Version {group.First().Version} has conflicting publication types: {string.Join(", ", publicationTypes)}" + ); + } + + // Keep the first entry, folding any same-version same-type duplicates + verifiedReleases.Add(group.First()); + } + return verifiedReleases; + } +} + +[JsonSourceGenerationOptions( + AllowTrailingCommas = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, + ReadCommentHandling = JsonCommentHandling.Skip, + UseStringEnumConverter = true, + WriteIndented = true, + NewLine = "\n" +)] +[JsonSerializable(typeof(ReleasesJsonSchema))] +internal partial class ReleasesJsonContext : JsonSerializerContext; diff --git a/CreateMatrix/VersionInfo.cs b/CreateMatrix/VersionInfo.cs index ada52c1d..1c3dd7fb 100644 --- a/CreateMatrix/VersionInfo.cs +++ b/CreateMatrix/VersionInfo.cs @@ -1,67 +1,67 @@ -namespace CreateMatrix; - -internal sealed class VersionInfo -{ - public enum LabelType - { - None, - Stable, - Latest, - Beta, - RC, - } - - public string Version { get; set; } = string.Empty; - public string UriX64 { get; set; } = string.Empty; - public string UriArm64 { get; set; } = string.Empty; - public List<LabelType> Labels { get; set; } = []; - - public int GetBuildNumber() => - // Extract the build number using the Version class (vs. regex) - // 5.0.0.35271 -> 35271 - // 5.1.0.35151 R1 -> 35151 - ParseVersion(Version).Revision; - - public void SetVersion(string version) => - // Store the normalized version number - Version = NormalizeVersion(version); - - public static string NormalizeVersion(string version) - { - // Remove the " Rxx" suffix from the version string - // "5.0.0.35134 R10" -> "5.0.0.35134" - int spaceIndex = version.IndexOf(' ', StringComparison.Ordinal); - return spaceIndex == -1 ? version : version[..spaceIndex]; - } - - public static Version ParseVersion(string version) => - // Parse the version number using the Version class, ignoring any " Rxx" suffix - new(NormalizeVersion(version)); - - public int CompareTo(VersionInfo rhs) => Compare(this, rhs); - - public int CompareTo(string rhs) => Compare(Version, rhs); - - public static int Compare(string lhs, string rhs) => - // Compare version numbers using Version class - ParseVersion(lhs).CompareTo(ParseVersion(rhs)); - - public static int Compare(VersionInfo lhs, VersionInfo rhs) => - Compare(lhs.Version, rhs.Version); - - public static IEnumerable<LabelType> GetLabelTypes() => - // Create list of label types - [.. Enum.GetValues<LabelType>().Where(labelType => labelType != LabelType.None)]; -} - -internal sealed class VersionInfoComparer : Comparer<VersionInfo> -{ - // Compare using version numbers - public override int Compare(VersionInfo? x, VersionInfo? y) => - x switch - { - null when y == null => 0, - null => -1, - _ => y == null ? 1 : VersionInfo.Compare(x, y), - }; -} +namespace CreateMatrix; + +internal sealed class VersionInfo +{ + public enum LabelType + { + None, + Stable, + Latest, + Beta, + RC, + } + + public string Version { get; set; } = string.Empty; + public string UriX64 { get; set; } = string.Empty; + public string UriArm64 { get; set; } = string.Empty; + public List<LabelType> Labels { get; set; } = []; + + public int GetBuildNumber() => + // Extract the build number using the Version class (vs. regex) + // 5.0.0.35271 -> 35271 + // 5.1.0.35151 R1 -> 35151 + ParseVersion(Version).Revision; + + public void SetVersion(string version) => + // Store the normalized version number + Version = NormalizeVersion(version); + + public static string NormalizeVersion(string version) + { + // Remove the " Rxx" suffix from the version string + // "5.0.0.35134 R10" -> "5.0.0.35134" + int spaceIndex = version.IndexOf(' ', StringComparison.Ordinal); + return spaceIndex == -1 ? version : version[..spaceIndex]; + } + + public static Version ParseVersion(string version) => + // Parse the version number using the Version class, ignoring any " Rxx" suffix + new(NormalizeVersion(version)); + + public int CompareTo(VersionInfo rhs) => Compare(this, rhs); + + public int CompareTo(string rhs) => Compare(Version, rhs); + + public static int Compare(string lhs, string rhs) => + // Compare version numbers using Version class + ParseVersion(lhs).CompareTo(ParseVersion(rhs)); + + public static int Compare(VersionInfo lhs, VersionInfo rhs) => + Compare(lhs.Version, rhs.Version); + + public static IEnumerable<LabelType> GetLabelTypes() => + // Create list of label types + [.. Enum.GetValues<LabelType>().Where(labelType => labelType != LabelType.None)]; +} + +internal sealed class VersionInfoComparer : Comparer<VersionInfo> +{ + // Compare using version numbers + public override int Compare(VersionInfo? x, VersionInfo? y) => + x switch + { + null when y == null => 0, + null => -1, + _ => y == null ? 1 : VersionInfo.Compare(x, y), + }; +} diff --git a/CreateMatrix/VersionJsonSchema.cs b/CreateMatrix/VersionJsonSchema.cs index 168b2d01..eba17e11 100644 --- a/CreateMatrix/VersionJsonSchema.cs +++ b/CreateMatrix/VersionJsonSchema.cs @@ -1,72 +1,72 @@ -namespace CreateMatrix; - -internal class VersionJsonSchemaBase -{ - [JsonRequired] - [JsonPropertyOrder(-2)] - public int SchemaVersion { get; set; } = VersionJsonSchema.Version; -} - -internal class VersionJsonSchema : VersionJsonSchemaBase -{ - public const int Version = 2; - - [JsonRequired] - public List<ProductInfo> Products { get; set; } = []; - - public static VersionJsonSchema FromFile(string path) => FromJson(File.ReadAllText(path)); - - public static void ToFile(string path, VersionJsonSchema json) - { - json.SchemaVersion = Version; - File.WriteAllText(path, ToJson(json)); - } - - private static string ToJson(VersionJsonSchema json) => - JsonSerializer.Serialize(json, VersionJsonContext.Default.VersionJsonSchema); - - private static VersionJsonSchema FromJson(string json) - { - VersionJsonSchemaBase? versionJsonSchemaBase = JsonSerializer.Deserialize( - json, - VersionJsonContext.Default.VersionJsonSchemaBase - ); - ArgumentNullException.ThrowIfNull(versionJsonSchemaBase); - - // Deserialize the correct version - int schemaVersion = versionJsonSchemaBase.SchemaVersion; - switch (schemaVersion) - { - case Version: - VersionJsonSchema? schema = JsonSerializer.Deserialize( - json, - VersionJsonContext.Default.VersionJsonSchema - ); - ArgumentNullException.ThrowIfNull(schema); - return schema; - // case 1: - // VersionInfo::Uri was replaced with UriX64 and UriArm64 was added - // Breaking change, UriArm64 is required in ARM64 docker builds - // Unknown version - default: - throw new NotSupportedException( - $"Unsupported schema version: {schemaVersion} (expected {Version})" - ); - } - } -} - -[JsonSourceGenerationOptions( - AllowTrailingCommas = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - IncludeFields = true, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, - ReadCommentHandling = JsonCommentHandling.Skip, - UseStringEnumConverter = true, - WriteIndented = true, - NewLine = "\r\n" -)] -[JsonSerializable(typeof(VersionJsonSchema))] -[JsonSerializable(typeof(VersionJsonSchemaBase))] -internal partial class VersionJsonContext : JsonSerializerContext; +namespace CreateMatrix; + +internal class VersionJsonSchemaBase +{ + [JsonRequired] + [JsonPropertyOrder(-2)] + public int SchemaVersion { get; set; } = VersionJsonSchema.Version; +} + +internal class VersionJsonSchema : VersionJsonSchemaBase +{ + public const int Version = 2; + + [JsonRequired] + public List<ProductInfo> Products { get; set; } = []; + + public static VersionJsonSchema FromFile(string path) => FromJson(File.ReadAllText(path)); + + public static void ToFile(string path, VersionJsonSchema json) + { + json.SchemaVersion = Version; + File.WriteAllText(path, ToJson(json)); + } + + private static string ToJson(VersionJsonSchema json) => + JsonSerializer.Serialize(json, VersionJsonContext.Default.VersionJsonSchema); + + private static VersionJsonSchema FromJson(string json) + { + VersionJsonSchemaBase? versionJsonSchemaBase = JsonSerializer.Deserialize( + json, + VersionJsonContext.Default.VersionJsonSchemaBase + ); + ArgumentNullException.ThrowIfNull(versionJsonSchemaBase); + + // Deserialize the correct version + int schemaVersion = versionJsonSchemaBase.SchemaVersion; + switch (schemaVersion) + { + case Version: + VersionJsonSchema? schema = JsonSerializer.Deserialize( + json, + VersionJsonContext.Default.VersionJsonSchema + ); + ArgumentNullException.ThrowIfNull(schema); + return schema; + // case 1: + // VersionInfo::Uri was replaced with UriX64 and UriArm64 was added + // Breaking change, UriArm64 is required in ARM64 docker builds + // Unknown version + default: + throw new NotSupportedException( + $"Unsupported schema version: {schemaVersion} (expected {Version})" + ); + } + } +} + +[JsonSourceGenerationOptions( + AllowTrailingCommas = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, + ReadCommentHandling = JsonCommentHandling.Skip, + UseStringEnumConverter = true, + WriteIndented = true, + NewLine = "\n" +)] +[JsonSerializable(typeof(VersionJsonSchema))] +[JsonSerializable(typeof(VersionJsonSchemaBase))] +internal partial class VersionJsonContext : JsonSerializerContext; diff --git a/CreateMatrixTests/.editorconfig b/CreateMatrixTests/.editorconfig index 1b0e9335..94be680f 100644 --- a/CreateMatrixTests/.editorconfig +++ b/CreateMatrixTests/.editorconfig @@ -1,19 +1,19 @@ -root = false - -# C# files -[*.cs] - -# Allow underscores in test method names -dotnet_diagnostic.CA1707.severity = none - -# Ignore unused private members -dotnet_diagnostic.IDE0052.severity = none - -# Ignore expression value is never used -dotnet_diagnostic.IDE0058.severity = none - -# Ignore missing XML docs for public test APIs -dotnet_diagnostic.CS1591.severity = none - -# Ignore making public types internal -dotnet_diagnostic.CA1515.severity = none +root = false + +# C# files +[*.cs] + +# Allow underscores in test method names +dotnet_diagnostic.CA1707.severity = none + +# Ignore unused private members +dotnet_diagnostic.IDE0052.severity = none + +# Ignore expression value is never used +dotnet_diagnostic.IDE0058.severity = none + +# Ignore missing XML docs for public test APIs +dotnet_diagnostic.CS1591.severity = none + +# Ignore making public types internal +dotnet_diagnostic.CA1515.severity = none diff --git a/CreateMatrixTests/CommandLineTests.cs b/CreateMatrixTests/CommandLineTests.cs index 2cc28804..cf6d8627 100644 --- a/CreateMatrixTests/CommandLineTests.cs +++ b/CreateMatrixTests/CommandLineTests.cs @@ -1,115 +1,115 @@ -using CreateMatrix; - -namespace CreateMatrixTests; - -public sealed class CommandLineTests -{ - [Fact] - public void CreateOptions_UsesDefaultsForMakeCommand() - { - DirectoryInfo tempDirectory = CreateTempDirectory(); - try - { - FileInfo versionFile = new(Path.Combine(tempDirectory.FullName, "Version.json")); - File.WriteAllText(versionFile.FullName, "{}"); - DirectoryInfo makeDirectory = new(Path.Combine(tempDirectory.FullName, "Make")); - DirectoryInfo dockerDirectory = new(Path.Combine(tempDirectory.FullName, "Docker")); - makeDirectory.Create(); - dockerDirectory.Create(); - - CommandLine commandLine = new([ - "make", - $"--versionpath={versionFile.FullName}", - $"--makedirectory={makeDirectory.FullName}", - $"--dockerdirectory={dockerDirectory.FullName}", - ]); - - CommandLine.Options options = CommandLine.CreateOptions(commandLine.Result); - - options.VersionLabel.Should().Be(VersionInfo.LabelType.Latest); - options.VersionPath?.FullName.Should().Be(versionFile.FullName); - options.MakeDirectory?.FullName.Should().Be(makeDirectory.FullName); - options.DockerDirectory?.FullName.Should().Be(dockerDirectory.FullName); - } - finally - { - DeleteTempDirectory(tempDirectory); - } - } - - [Fact] - public void CreateOptions_ParsesExplicitVersionLabel() - { - DirectoryInfo tempDirectory = CreateTempDirectory(); - try - { - FileInfo versionFile = new(Path.Combine(tempDirectory.FullName, "Version.json")); - File.WriteAllText(versionFile.FullName, "{}"); - DirectoryInfo makeDirectory = new(Path.Combine(tempDirectory.FullName, "Make")); - DirectoryInfo dockerDirectory = new(Path.Combine(tempDirectory.FullName, "Docker")); - makeDirectory.Create(); - dockerDirectory.Create(); - - CommandLine commandLine = new([ - "make", - $"--versionpath={versionFile.FullName}", - $"--makedirectory={makeDirectory.FullName}", - $"--dockerdirectory={dockerDirectory.FullName}", - "--versionlabel=Beta", - ]); - - CommandLine.Options options = CommandLine.CreateOptions(commandLine.Result); - - options.VersionLabel.Should().Be(VersionInfo.LabelType.Beta); - } - finally - { - DeleteTempDirectory(tempDirectory); - } - } - - [Fact] - public void BypassStartup_ReturnsTrueOnHelp() - { - CommandLine commandLine = new(["--help"]); - - bool bypass = CommandLine.BypassStartup(commandLine.Result); - - bypass.Should().BeTrue(); - } - - [Fact] - public void BypassStartup_ReturnsTrueOnParseErrors() - { - CommandLine commandLine = new([ - "matrix", - "--versionpath=missing.json", - "--matrixpath=matrix.json", - ]); - - bool bypass = CommandLine.BypassStartup(commandLine.Result); - - bypass.Should().BeTrue(); - } - - private static DirectoryInfo CreateTempDirectory() - { - string directoryPath = Path.Combine( - Path.GetTempPath(), - "CreateMatrixTests", - Guid.NewGuid().ToString("N") - ); - DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath); - return directoryInfo; - } - - private static void DeleteTempDirectory(DirectoryInfo directoryInfo) - { - if (!directoryInfo.Exists) - { - return; - } - - directoryInfo.Delete(true); - } -} +using CreateMatrix; + +namespace CreateMatrixTests; + +public sealed class CommandLineTests +{ + [Fact] + public void CreateOptions_UsesDefaultsForMakeCommand() + { + DirectoryInfo tempDirectory = CreateTempDirectory(); + try + { + FileInfo versionFile = new(Path.Combine(tempDirectory.FullName, "Version.json")); + File.WriteAllText(versionFile.FullName, "{}"); + DirectoryInfo makeDirectory = new(Path.Combine(tempDirectory.FullName, "Make")); + DirectoryInfo dockerDirectory = new(Path.Combine(tempDirectory.FullName, "Docker")); + makeDirectory.Create(); + dockerDirectory.Create(); + + CommandLine commandLine = new([ + "make", + $"--versionpath={versionFile.FullName}", + $"--makedirectory={makeDirectory.FullName}", + $"--dockerdirectory={dockerDirectory.FullName}", + ]); + + CommandLine.Options options = CommandLine.CreateOptions(commandLine.Result); + + options.VersionLabel.Should().Be(VersionInfo.LabelType.Latest); + options.VersionPath?.FullName.Should().Be(versionFile.FullName); + options.MakeDirectory?.FullName.Should().Be(makeDirectory.FullName); + options.DockerDirectory?.FullName.Should().Be(dockerDirectory.FullName); + } + finally + { + DeleteTempDirectory(tempDirectory); + } + } + + [Fact] + public void CreateOptions_ParsesExplicitVersionLabel() + { + DirectoryInfo tempDirectory = CreateTempDirectory(); + try + { + FileInfo versionFile = new(Path.Combine(tempDirectory.FullName, "Version.json")); + File.WriteAllText(versionFile.FullName, "{}"); + DirectoryInfo makeDirectory = new(Path.Combine(tempDirectory.FullName, "Make")); + DirectoryInfo dockerDirectory = new(Path.Combine(tempDirectory.FullName, "Docker")); + makeDirectory.Create(); + dockerDirectory.Create(); + + CommandLine commandLine = new([ + "make", + $"--versionpath={versionFile.FullName}", + $"--makedirectory={makeDirectory.FullName}", + $"--dockerdirectory={dockerDirectory.FullName}", + "--versionlabel=Beta", + ]); + + CommandLine.Options options = CommandLine.CreateOptions(commandLine.Result); + + options.VersionLabel.Should().Be(VersionInfo.LabelType.Beta); + } + finally + { + DeleteTempDirectory(tempDirectory); + } + } + + [Fact] + public void BypassStartup_ReturnsTrueOnHelp() + { + CommandLine commandLine = new(["--help"]); + + bool bypass = CommandLine.BypassStartup(commandLine.Result); + + bypass.Should().BeTrue(); + } + + [Fact] + public void BypassStartup_ReturnsTrueOnParseErrors() + { + CommandLine commandLine = new([ + "matrix", + "--versionpath=missing.json", + "--matrixpath=matrix.json", + ]); + + bool bypass = CommandLine.BypassStartup(commandLine.Result); + + bypass.Should().BeTrue(); + } + + private static DirectoryInfo CreateTempDirectory() + { + string directoryPath = Path.Combine( + Path.GetTempPath(), + "CreateMatrixTests", + Guid.NewGuid().ToString("N") + ); + DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath); + return directoryInfo; + } + + private static void DeleteTempDirectory(DirectoryInfo directoryInfo) + { + if (!directoryInfo.Exists) + { + return; + } + + directoryInfo.Delete(true); + } +} diff --git a/CreateMatrixTests/ComposeFileTests.cs b/CreateMatrixTests/ComposeFileTests.cs index 25d3ed0e..577d3a31 100644 --- a/CreateMatrixTests/ComposeFileTests.cs +++ b/CreateMatrixTests/ComposeFileTests.cs @@ -1,76 +1,76 @@ -using CreateMatrix; - -namespace CreateMatrixTests; - -public sealed class ComposeFileTests -{ - [Fact] - public void Create_WritesAllComposeFiles() - { - DirectoryInfo tempDirectory = CreateTempDirectory(); - try - { - ComposeFile.Create(tempDirectory); - - string testFile = Path.Combine(tempDirectory.FullName, "Test.yml"); - string developFile = Path.Combine(tempDirectory.FullName, "Test-develop.yml"); - string latestFile = Path.Combine(tempDirectory.FullName, "Test-latest.yml"); - - bool testExists = File.Exists(testFile); - bool developExists = File.Exists(developFile); - bool latestExists = File.Exists(latestFile); - - testExists.Should().BeTrue(); - developExists.Should().BeTrue(); - latestExists.Should().BeTrue(); - } - finally - { - DeleteTempDirectory(tempDirectory); - } - } - - [Fact] - public void Create_UsesLabelForTaggedImages() - { - DirectoryInfo tempDirectory = CreateTempDirectory(); - try - { - ComposeFile.Create(tempDirectory); - - string testFile = Path.Combine(tempDirectory.FullName, "Test.yml"); - string latestFile = Path.Combine(tempDirectory.FullName, "Test-latest.yml"); - - string testContents = File.ReadAllText(testFile); - string latestContents = File.ReadAllText(latestFile); - - testContents.Should().Contain("image: test_nxwitness"); - latestContents.Should().Contain("image: docker.io/ptr727/nxwitness:latest"); - } - finally - { - DeleteTempDirectory(tempDirectory); - } - } - - private static DirectoryInfo CreateTempDirectory() - { - string directoryPath = Path.Combine( - Path.GetTempPath(), - "CreateMatrixTests", - Guid.NewGuid().ToString("N") - ); - DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath); - return directoryInfo; - } - - private static void DeleteTempDirectory(DirectoryInfo directoryInfo) - { - if (!directoryInfo.Exists) - { - return; - } - - directoryInfo.Delete(true); - } -} +using CreateMatrix; + +namespace CreateMatrixTests; + +public sealed class ComposeFileTests +{ + [Fact] + public void Create_WritesAllComposeFiles() + { + DirectoryInfo tempDirectory = CreateTempDirectory(); + try + { + ComposeFile.Create(tempDirectory); + + string testFile = Path.Combine(tempDirectory.FullName, "Test.yml"); + string developFile = Path.Combine(tempDirectory.FullName, "Test-develop.yml"); + string latestFile = Path.Combine(tempDirectory.FullName, "Test-latest.yml"); + + bool testExists = File.Exists(testFile); + bool developExists = File.Exists(developFile); + bool latestExists = File.Exists(latestFile); + + testExists.Should().BeTrue(); + developExists.Should().BeTrue(); + latestExists.Should().BeTrue(); + } + finally + { + DeleteTempDirectory(tempDirectory); + } + } + + [Fact] + public void Create_UsesLabelForTaggedImages() + { + DirectoryInfo tempDirectory = CreateTempDirectory(); + try + { + ComposeFile.Create(tempDirectory); + + string testFile = Path.Combine(tempDirectory.FullName, "Test.yml"); + string latestFile = Path.Combine(tempDirectory.FullName, "Test-latest.yml"); + + string testContents = File.ReadAllText(testFile); + string latestContents = File.ReadAllText(latestFile); + + testContents.Should().Contain("image: test_nxwitness"); + latestContents.Should().Contain("image: docker.io/ptr727/nxwitness:latest"); + } + finally + { + DeleteTempDirectory(tempDirectory); + } + } + + private static DirectoryInfo CreateTempDirectory() + { + string directoryPath = Path.Combine( + Path.GetTempPath(), + "CreateMatrixTests", + Guid.NewGuid().ToString("N") + ); + DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath); + return directoryInfo; + } + + private static void DeleteTempDirectory(DirectoryInfo directoryInfo) + { + if (!directoryInfo.Exists) + { + return; + } + + directoryInfo.Delete(true); + } +} diff --git a/CreateMatrixTests/CreateMatrixTests.csproj b/CreateMatrixTests/CreateMatrixTests.csproj index 0c328567..fe1756a4 100644 --- a/CreateMatrixTests/CreateMatrixTests.csproj +++ b/CreateMatrixTests/CreateMatrixTests.csproj @@ -1,27 +1,23 @@ -<Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <AnalysisLevel>latest-all</AnalysisLevel> - <EnableNETAnalyzers>true</EnableNETAnalyzers> - <IsPackable>false</IsPackable> - <IsTestProject>true</IsTestProject> - <Nullable>enable</Nullable> - <TargetFramework>net10.0</TargetFramework> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="AwesomeAssertions" Version="9.5.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" /> - <PackageReference Include="xunit.v3" Version="3.2.2" /> - <PackageReference Include="xunit.analyzers" Version="1.27.0"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - </PackageReference> - <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" PrivateAssets="All" /> - <PackageReference Include="coverlet.collector" Version="10.0.1"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - </ItemGroup> - <ItemGroup> - <ProjectReference Include="..\CreateMatrix\CreateMatrix.csproj" /> - </ItemGroup> -</Project> +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <AnalysisLevel>latest-all</AnalysisLevel> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + <IsPackable>false</IsPackable> + <IsTestProject>true</IsTestProject> + <Nullable>enable</Nullable> + <TargetFramework>net10.0</TargetFramework> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="AwesomeAssertions" Version="9.6.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" /> + <PackageReference Include="xunit.v3" Version="4.0.0" /> + <PackageReference Include="xunit.analyzers" Version="2.0.0"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.9.0" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\CreateMatrix\CreateMatrix.csproj" /> + </ItemGroup> +</Project> diff --git a/CreateMatrixTests/DockerFileTests.cs b/CreateMatrixTests/DockerFileTests.cs index c204645b..6197a595 100644 --- a/CreateMatrixTests/DockerFileTests.cs +++ b/CreateMatrixTests/DockerFileTests.cs @@ -1,126 +1,126 @@ -using CreateMatrix; - -namespace CreateMatrixTests; - -public sealed class DockerFileTests -{ - [Fact] - public void Create_WritesDockerfilesForAllProducts() - { - DirectoryInfo tempDirectory = CreateTempDirectory(); - try - { - List<ProductInfo> products = CreateProducts(); - - DockerFile.Create(products, tempDirectory.FullName, VersionInfo.LabelType.Latest); - - foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) - { - string standardFile = Path.Combine( - tempDirectory.FullName, - $"{ProductInfo.GetDocker(productType, false)}.Dockerfile" - ); - string lsioFile = Path.Combine( - tempDirectory.FullName, - $"{ProductInfo.GetDocker(productType, true)}.Dockerfile" - ); - - bool standardExists = File.Exists(standardFile); - bool lsioExists = File.Exists(lsioFile); - standardExists.Should().BeTrue(); - lsioExists.Should().BeTrue(); - } - } - finally - { - DeleteTempDirectory(tempDirectory); - } - } - - [Fact] - public void Create_UsesLatestWhenLabelMissing() - { - DirectoryInfo tempDirectory = CreateTempDirectory(); - try - { - List<ProductInfo> products = CreateProducts(); - - DockerFile.Create(products, tempDirectory.FullName, VersionInfo.LabelType.Beta); - - string nxGoFile = Path.Combine( - tempDirectory.FullName, - $"{ProductInfo.GetDocker(ProductInfo.ProductType.NxGo, false)}.Dockerfile" - ); - string nxMetaFile = Path.Combine( - tempDirectory.FullName, - $"{ProductInfo.GetDocker(ProductInfo.ProductType.NxMeta, false)}.Dockerfile" - ); - - string nxGoContents = File.ReadAllText(nxGoFile); - string nxMetaContents = File.ReadAllText(nxMetaFile); - - nxGoContents.Should().Contain("ARG LABEL_VERSION=\"5.1.0.12345\""); - nxMetaContents.Should().Contain("ARG LABEL_VERSION=\"5.2.0.23456\""); - } - finally - { - DeleteTempDirectory(tempDirectory); - } - } - - private static List<ProductInfo> CreateProducts() - { - List<ProductInfo> products = []; - foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) - { - ProductInfo productInfo = new() { Product = productType }; - - VersionInfo latest = new() - { - Version = "5.1.0.12345", - UriX64 = "https://example.com/x64.deb", - UriArm64 = "https://example.com/arm64.deb", - }; - latest.Labels.Add(VersionInfo.LabelType.Latest); - latest.Labels.Add(VersionInfo.LabelType.Stable); - productInfo.Versions.Add(latest); - - if (productType == ProductInfo.ProductType.NxMeta) - { - VersionInfo beta = new() - { - Version = "5.2.0.23456", - UriX64 = "https://example.com/x64-beta.deb", - UriArm64 = "https://example.com/arm64-beta.deb", - }; - beta.Labels.Add(VersionInfo.LabelType.Beta); - productInfo.Versions.Add(beta); - } - - products.Add(productInfo); - } - - return products; - } - - private static DirectoryInfo CreateTempDirectory() - { - string directoryPath = Path.Combine( - Path.GetTempPath(), - "CreateMatrixTests", - Guid.NewGuid().ToString("N") - ); - DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath); - return directoryInfo; - } - - private static void DeleteTempDirectory(DirectoryInfo directoryInfo) - { - if (!directoryInfo.Exists) - { - return; - } - - directoryInfo.Delete(true); - } -} +using CreateMatrix; + +namespace CreateMatrixTests; + +public sealed class DockerFileTests +{ + [Fact] + public void Create_WritesDockerfilesForAllProducts() + { + DirectoryInfo tempDirectory = CreateTempDirectory(); + try + { + List<ProductInfo> products = CreateProducts(); + + DockerFile.Create(products, tempDirectory.FullName, VersionInfo.LabelType.Latest); + + foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) + { + string standardFile = Path.Combine( + tempDirectory.FullName, + $"{ProductInfo.GetDocker(productType, false)}.Dockerfile" + ); + string lsioFile = Path.Combine( + tempDirectory.FullName, + $"{ProductInfo.GetDocker(productType, true)}.Dockerfile" + ); + + bool standardExists = File.Exists(standardFile); + bool lsioExists = File.Exists(lsioFile); + standardExists.Should().BeTrue(); + lsioExists.Should().BeTrue(); + } + } + finally + { + DeleteTempDirectory(tempDirectory); + } + } + + [Fact] + public void Create_UsesLatestWhenLabelMissing() + { + DirectoryInfo tempDirectory = CreateTempDirectory(); + try + { + List<ProductInfo> products = CreateProducts(); + + DockerFile.Create(products, tempDirectory.FullName, VersionInfo.LabelType.Beta); + + string nxGoFile = Path.Combine( + tempDirectory.FullName, + $"{ProductInfo.GetDocker(ProductInfo.ProductType.NxGo, false)}.Dockerfile" + ); + string nxMetaFile = Path.Combine( + tempDirectory.FullName, + $"{ProductInfo.GetDocker(ProductInfo.ProductType.NxMeta, false)}.Dockerfile" + ); + + string nxGoContents = File.ReadAllText(nxGoFile); + string nxMetaContents = File.ReadAllText(nxMetaFile); + + nxGoContents.Should().Contain("ARG LABEL_VERSION=\"5.1.0.12345\""); + nxMetaContents.Should().Contain("ARG LABEL_VERSION=\"5.2.0.23456\""); + } + finally + { + DeleteTempDirectory(tempDirectory); + } + } + + private static List<ProductInfo> CreateProducts() + { + List<ProductInfo> products = []; + foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) + { + ProductInfo productInfo = new() { Product = productType }; + + VersionInfo latest = new() + { + Version = "5.1.0.12345", + UriX64 = "https://example.com/x64.deb", + UriArm64 = "https://example.com/arm64.deb", + }; + latest.Labels.Add(VersionInfo.LabelType.Latest); + latest.Labels.Add(VersionInfo.LabelType.Stable); + productInfo.Versions.Add(latest); + + if (productType == ProductInfo.ProductType.NxMeta) + { + VersionInfo beta = new() + { + Version = "5.2.0.23456", + UriX64 = "https://example.com/x64-beta.deb", + UriArm64 = "https://example.com/arm64-beta.deb", + }; + beta.Labels.Add(VersionInfo.LabelType.Beta); + productInfo.Versions.Add(beta); + } + + products.Add(productInfo); + } + + return products; + } + + private static DirectoryInfo CreateTempDirectory() + { + string directoryPath = Path.Combine( + Path.GetTempPath(), + "CreateMatrixTests", + Guid.NewGuid().ToString("N") + ); + DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath); + return directoryInfo; + } + + private static void DeleteTempDirectory(DirectoryInfo directoryInfo) + { + if (!directoryInfo.Exists) + { + return; + } + + directoryInfo.Delete(true); + } +} diff --git a/CreateMatrixTests/Fixture.cs b/CreateMatrixTests/Fixture.cs index 9fb93050..065a8fd1 100644 --- a/CreateMatrixTests/Fixture.cs +++ b/CreateMatrixTests/Fixture.cs @@ -1,18 +1,18 @@ -// Single instance for all tests in assembly -[assembly: AssemblyFixture(typeof(CreateMatrixTests.SingleInstanceFixture))] - -namespace CreateMatrixTests; - -// Sequential execution fixture -[CollectionDefinition("Sequential Test Collection", DisableParallelization = true)] -public class SequentialCollectionDefinition; - -[System.Diagnostics.CodeAnalysis.SuppressMessage( - "Design", - "CA1063:Implement IDisposable Correctly", - Justification = "Demonstration only" -)] -public class SingleInstanceFixture : IDisposable -{ - public void Dispose() => GC.SuppressFinalize(this); -} +// Single instance for all tests in assembly +[assembly: AssemblyFixture(typeof(CreateMatrixTests.SingleInstanceFixture))] + +namespace CreateMatrixTests; + +// Sequential execution fixture +[CollectionDefinition("Sequential Test Collection", DisableParallelization = true)] +public class SequentialCollectionDefinition; + +[System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1063:Implement IDisposable Correctly", + Justification = "Demonstration only" +)] +public class SingleInstanceFixture : IDisposable +{ + public void Dispose() => GC.SuppressFinalize(this); +} diff --git a/CreateMatrixTests/GlobalUsings.cs b/CreateMatrixTests/GlobalUsings.cs index 7d75b0c6..ab417b3c 100644 --- a/CreateMatrixTests/GlobalUsings.cs +++ b/CreateMatrixTests/GlobalUsings.cs @@ -1,6 +1,6 @@ -global using System; -global using System.Collections.Generic; -global using System.IO; -global using System.Linq; -global using AwesomeAssertions; -global using Xunit; +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using AwesomeAssertions; +global using Xunit; diff --git a/CreateMatrixTests/ReleasesTests.cs b/CreateMatrixTests/ReleasesTests.cs index 0e6e9ec7..3cb33ff3 100644 --- a/CreateMatrixTests/ReleasesTests.cs +++ b/CreateMatrixTests/ReleasesTests.cs @@ -1,322 +1,322 @@ -using CreateMatrix; - -namespace CreateMatrixTests; - -public sealed class ReleasesTests -{ - [Fact] - public void MatchLabels() - { - // Create test releases - ReleasesJsonSchema releasesSchema = new() - { - Releases = - { - // Stable, published and released - new Release - { - PublicationType = Release.ReleasePublication, - ReleaseDate = 1, - ReleaseDeliveryDays = 1, - Version = "1.0", - }, - // Latest, published not released - new Release { PublicationType = Release.ReleasePublication, Version = "2.0" }, - // RC - new Release { PublicationType = Release.RcPublication, Version = "3.0" }, - // Beta - new Release { PublicationType = Release.BetaPublication, Version = "4.0" }, - }, - }; - - // Create ProductInfo from schema - ProductInfo productInfo = CreateProductInfo(releasesSchema); - - // 4 versions - productInfo.Versions.Should().HaveCount(4); - // 1 Latest - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) - .Should() - .Be(1); - // 1 Stable - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) - .Should() - .Be(1); - // 1 RC - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) - .Should() - .Be(1); - // 1 Beta - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) - .Should() - .Be(1); - // 1 label per version - productInfo.Versions.Count(item => item.Labels.Count == 1).Should().Be(4); - } - - [Fact] - public void MissingLatest() - { - // Similar to MissingStable() - - // Create test releases - ReleasesJsonSchema releasesSchema = new() - { - Releases = - { - // Stable, published and released - new Release - { - PublicationType = Release.ReleasePublication, - ReleaseDate = 1, - ReleaseDeliveryDays = 1, - Version = "1.0", - }, - // RC - new Release { PublicationType = Release.RcPublication, Version = "3.0" }, - // Beta - new Release { PublicationType = Release.BetaPublication, Version = "4.0" }, - }, - }; - - // Create ProductInfo from schema - ProductInfo productInfo = CreateProductInfo(releasesSchema); - - // 3 versions - productInfo.Versions.Should().HaveCount(3); - // 1 Latest - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) - .Should() - .Be(1); - // 1 Stable - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) - .Should() - .Be(1); - // 1 RC - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) - .Should() - .Be(1); - // 1 Beta - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) - .Should() - .Be(1); - - // Select all Latest or Stable labels - IEnumerable<VersionInfo> latestVersions = productInfo.Versions.Where(item => - item.Labels.Contains(VersionInfo.LabelType.Latest) - || item.Labels.Contains(VersionInfo.LabelType.Stable) - ); - // Should just be 1 entry - latestVersions.Should().ContainSingle(); - // Should have Latest and Stable labels - VersionInfo version = latestVersions.First(); - version.Labels.Count.Should().Be(2); - } - - [Fact] - public void MissingStable() - { - // Similar to MissingLatest() - - // Create test releases - ReleasesJsonSchema releasesSchema = new() - { - Releases = - { - // Latest, published not released - new Release { PublicationType = Release.ReleasePublication, Version = "1.0" }, - // RC - new Release { PublicationType = Release.RcPublication, Version = "3.0" }, - // Beta - new Release { PublicationType = Release.BetaPublication, Version = "4.0" }, - }, - }; - - // Create ProductInfo from schema - ProductInfo productInfo = CreateProductInfo(releasesSchema); - - // 3 versions - productInfo.Versions.Should().HaveCount(3); - // 1 Latest - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) - .Should() - .Be(1); - // 1 Stable - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) - .Should() - .Be(1); - // 1 RC - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) - .Should() - .Be(1); - // 1 Beta - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) - .Should() - .Be(1); - - // Select all Latest or Stable labels - IEnumerable<VersionInfo> latestVersions = productInfo.Versions.Where(item => - item.Labels.Contains(VersionInfo.LabelType.Latest) - || item.Labels.Contains(VersionInfo.LabelType.Stable) - ); - // Should just be 1 entry - latestVersions.Should().ContainSingle(); - // Should have Latest and Stable labels - VersionInfo version = latestVersions.First(); - version.Labels.Count.Should().Be(2); - } - - [Fact] - public void MultipleReleases() - { - // Create test releases - ReleasesJsonSchema releasesSchema = new() - { - Releases = - { - // Published not released - new Release { PublicationType = Release.ReleasePublication, Version = "2.0" }, - new Release { PublicationType = Release.ReleasePublication, Version = "3.0" }, - new Release { PublicationType = Release.ReleasePublication, Version = "4.0" }, - }, - }; - - // Create ProductInfo from schema - ProductInfo productInfo = CreateProductInfo(releasesSchema); - - // 1 version - productInfo.Versions.Should().ContainSingle(); - // 2 labels per version - productInfo.Versions.Count(item => item.Labels.Count == 2).Should().Be(1); - // 1 Latest - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) - .Should() - .Be(1); - // 1 Stable - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) - .Should() - .Be(1); - - // Select all Latest or Stable labels - IEnumerable<VersionInfo> latestVersions = productInfo.Versions.Where(item => - item.Labels.Contains(VersionInfo.LabelType.Latest) - || item.Labels.Contains(VersionInfo.LabelType.Stable) - ); - // Should just be 1 entry - latestVersions.Should().ContainSingle(); - // Should have Latest and Stable labels - VersionInfo version = latestVersions.First(); - version.Labels.Count.Should().Be(2); - - // Should be the v4.0 version - version.Version.Should().Be("4.0"); - } - - [Fact] - public void ConflictingPublicationTypes_Throws() - { - // The same version number tagged with two different publication types is contradictory - // vendor data and must be rejected rather than folded into our data. - ReleasesJsonSchema releasesSchema = new() - { - Releases = - { - new Release - { - PublicationType = Release.ReleasePublication, - ReleaseDate = 1, - ReleaseDeliveryDays = 1, - Version = "6.1.2.42921", - }, - new Release { PublicationType = Release.BetaPublication, Version = "6.1.2.42921" }, - }, - }; - - Action act = () => CreateProductInfo(releasesSchema); - act.Should().Throw<InvalidOperationException>(); - } - - [Fact] - public void DuplicatePublicationType_Folds() - { - // The same version number listed twice with the same publication type is a benign - // duplicate and is folded to a single entry. - ReleasesJsonSchema releasesSchema = new() - { - Releases = - { - new Release - { - PublicationType = Release.ReleasePublication, - ReleaseDate = 1, - ReleaseDeliveryDays = 1, - Version = "6.1.2.42921", - }, - new Release - { - PublicationType = Release.ReleasePublication, - ReleaseDate = 1, - ReleaseDeliveryDays = 1, - Version = "6.1.2.42921", - }, - }, - }; - - ProductInfo productInfo = CreateProductInfo(releasesSchema); - - // Folded to a single version - productInfo.Versions.Should().ContainSingle(); - productInfo.Versions.First().Version.Should().Be("6.1.2.42921"); - } - - [Fact] - public void MissingVersion_Throws() - { - // A release with a missing or unparseable version must be rejected with a clear - // error rather than a context-free version-parsing failure. - ReleasesJsonSchema releasesSchema = new() - { - Releases = - { - new Release - { - PublicationType = Release.ReleasePublication, - ReleaseDate = 1, - ReleaseDeliveryDays = 1, - Version = "", - }, - }, - }; - - Action act = () => CreateProductInfo(releasesSchema); - act.Should().Throw<InvalidOperationException>(); - } - - private static ProductInfo CreateProductInfo(ReleasesJsonSchema releasesSchema) - { - // Mirror of the non-network portion of ProductInfo.FetchVersionsAsync(), - // sharing the version and label logic via ProductInfo.CreateVersionInfo(). - ProductInfo productInfo = new(); - foreach (Release release in ReleasesJsonSchema.VerifyReleases(releasesSchema.Releases)) - { - productInfo.Versions.Add(productInfo.CreateVersionInfo(release)); - } - productInfo.VerifyLabels(); - - return productInfo; - } -} +using CreateMatrix; + +namespace CreateMatrixTests; + +public sealed class ReleasesTests +{ + [Fact] + public void MatchLabels() + { + // Create test releases + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + // Stable, published and released + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "1.0", + }, + // Latest, published not released + new Release { PublicationType = Release.ReleasePublication, Version = "2.0" }, + // RC + new Release { PublicationType = Release.RcPublication, Version = "3.0" }, + // Beta + new Release { PublicationType = Release.BetaPublication, Version = "4.0" }, + }, + }; + + // Create ProductInfo from schema + ProductInfo productInfo = CreateProductInfo(releasesSchema); + + // 4 versions + productInfo.Versions.Should().HaveCount(4); + // 1 Latest + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) + .Should() + .Be(1); + // 1 Stable + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) + .Should() + .Be(1); + // 1 RC + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) + .Should() + .Be(1); + // 1 Beta + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) + .Should() + .Be(1); + // 1 label per version + productInfo.Versions.Count(item => item.Labels.Count == 1).Should().Be(4); + } + + [Fact] + public void MissingLatest() + { + // Similar to MissingStable() + + // Create test releases + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + // Stable, published and released + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "1.0", + }, + // RC + new Release { PublicationType = Release.RcPublication, Version = "3.0" }, + // Beta + new Release { PublicationType = Release.BetaPublication, Version = "4.0" }, + }, + }; + + // Create ProductInfo from schema + ProductInfo productInfo = CreateProductInfo(releasesSchema); + + // 3 versions + productInfo.Versions.Should().HaveCount(3); + // 1 Latest + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) + .Should() + .Be(1); + // 1 Stable + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) + .Should() + .Be(1); + // 1 RC + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) + .Should() + .Be(1); + // 1 Beta + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) + .Should() + .Be(1); + + // Select all Latest or Stable labels + IEnumerable<VersionInfo> latestVersions = productInfo.Versions.Where(item => + item.Labels.Contains(VersionInfo.LabelType.Latest) + || item.Labels.Contains(VersionInfo.LabelType.Stable) + ); + // Should just be 1 entry + latestVersions.Should().ContainSingle(); + // Should have Latest and Stable labels + VersionInfo version = latestVersions.First(); + version.Labels.Count.Should().Be(2); + } + + [Fact] + public void MissingStable() + { + // Similar to MissingLatest() + + // Create test releases + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + // Latest, published not released + new Release { PublicationType = Release.ReleasePublication, Version = "1.0" }, + // RC + new Release { PublicationType = Release.RcPublication, Version = "3.0" }, + // Beta + new Release { PublicationType = Release.BetaPublication, Version = "4.0" }, + }, + }; + + // Create ProductInfo from schema + ProductInfo productInfo = CreateProductInfo(releasesSchema); + + // 3 versions + productInfo.Versions.Should().HaveCount(3); + // 1 Latest + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) + .Should() + .Be(1); + // 1 Stable + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) + .Should() + .Be(1); + // 1 RC + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) + .Should() + .Be(1); + // 1 Beta + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) + .Should() + .Be(1); + + // Select all Latest or Stable labels + IEnumerable<VersionInfo> latestVersions = productInfo.Versions.Where(item => + item.Labels.Contains(VersionInfo.LabelType.Latest) + || item.Labels.Contains(VersionInfo.LabelType.Stable) + ); + // Should just be 1 entry + latestVersions.Should().ContainSingle(); + // Should have Latest and Stable labels + VersionInfo version = latestVersions.First(); + version.Labels.Count.Should().Be(2); + } + + [Fact] + public void MultipleReleases() + { + // Create test releases + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + // Published not released + new Release { PublicationType = Release.ReleasePublication, Version = "2.0" }, + new Release { PublicationType = Release.ReleasePublication, Version = "3.0" }, + new Release { PublicationType = Release.ReleasePublication, Version = "4.0" }, + }, + }; + + // Create ProductInfo from schema + ProductInfo productInfo = CreateProductInfo(releasesSchema); + + // 1 version + productInfo.Versions.Should().ContainSingle(); + // 2 labels per version + productInfo.Versions.Count(item => item.Labels.Count == 2).Should().Be(1); + // 1 Latest + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) + .Should() + .Be(1); + // 1 Stable + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) + .Should() + .Be(1); + + // Select all Latest or Stable labels + IEnumerable<VersionInfo> latestVersions = productInfo.Versions.Where(item => + item.Labels.Contains(VersionInfo.LabelType.Latest) + || item.Labels.Contains(VersionInfo.LabelType.Stable) + ); + // Should just be 1 entry + latestVersions.Should().ContainSingle(); + // Should have Latest and Stable labels + VersionInfo version = latestVersions.First(); + version.Labels.Count.Should().Be(2); + + // Should be the v4.0 version + version.Version.Should().Be("4.0"); + } + + [Fact] + public void ConflictingPublicationTypes_Throws() + { + // The same version number tagged with two different publication types is contradictory + // vendor data and must be rejected rather than folded into our data. + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "6.1.2.42921", + }, + new Release { PublicationType = Release.BetaPublication, Version = "6.1.2.42921" }, + }, + }; + + Action act = () => CreateProductInfo(releasesSchema); + act.Should().Throw<InvalidOperationException>(); + } + + [Fact] + public void DuplicatePublicationType_Folds() + { + // The same version number listed twice with the same publication type is a benign + // duplicate and is folded to a single entry. + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "6.1.2.42921", + }, + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "6.1.2.42921", + }, + }, + }; + + ProductInfo productInfo = CreateProductInfo(releasesSchema); + + // Folded to a single version + productInfo.Versions.Should().ContainSingle(); + productInfo.Versions.First().Version.Should().Be("6.1.2.42921"); + } + + [Fact] + public void MissingVersion_Throws() + { + // A release with a missing or unparseable version must be rejected with a clear + // error rather than a context-free version-parsing failure. + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "", + }, + }, + }; + + Action act = () => CreateProductInfo(releasesSchema); + act.Should().Throw<InvalidOperationException>(); + } + + private static ProductInfo CreateProductInfo(ReleasesJsonSchema releasesSchema) + { + // Mirror of the non-network portion of ProductInfo.FetchVersionsAsync(), + // sharing the version and label logic via ProductInfo.CreateVersionInfo(). + ProductInfo productInfo = new(); + foreach (Release release in ReleasesJsonSchema.VerifyReleases(releasesSchema.Releases)) + { + productInfo.Versions.Add(productInfo.CreateVersionInfo(release)); + } + productInfo.VerifyLabels(); + + return productInfo; + } +} diff --git a/CreateMatrixTests/VersionForwardTests.cs b/CreateMatrixTests/VersionForwardTests.cs index cc8e8f85..1b6d6b23 100644 --- a/CreateMatrixTests/VersionForwardTests.cs +++ b/CreateMatrixTests/VersionForwardTests.cs @@ -1,269 +1,269 @@ -using CreateMatrix; - -namespace CreateMatrixTests; - -public class VersionForwardTests -{ - [Fact] - public void VersionForward() - { - // Create test releases - List<ProductInfo> oldProductList = - [ - new ProductInfo - { - Product = ProductInfo.ProductType.NxMeta, - Versions = - { - new VersionInfo { Version = "1.0", Labels = { VersionInfo.LabelType.Stable } }, - new VersionInfo { Version = "2.0", Labels = { VersionInfo.LabelType.Latest } }, - new VersionInfo { Version = "3.0", Labels = { VersionInfo.LabelType.RC } }, - new VersionInfo { Version = "4.0", Labels = { VersionInfo.LabelType.Beta } }, - }, - }, - ]; - List<ProductInfo> newProductList = - [ - new ProductInfo - { - Product = ProductInfo.ProductType.NxMeta, - Versions = - { - new VersionInfo { Version = "1.1", Labels = { VersionInfo.LabelType.Stable } }, - new VersionInfo { Version = "2.1", Labels = { VersionInfo.LabelType.Latest } }, - new VersionInfo { Version = "3.1", Labels = { VersionInfo.LabelType.RC } }, - new VersionInfo { Version = "4.1", Labels = { VersionInfo.LabelType.Beta } }, - }, - }, - ]; - - // newProductList will be updated in-place - // Only Stable and Latest is tested - // Versions with multiple labels will update the version not the individual labels - ReleaseVersionForward.Verify(oldProductList, newProductList); - ProductInfo productInfo = newProductList.First(); - - // 4 versions - productInfo.Versions.Should().HaveCount(4); - // 1 Latest - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) - .Should() - .Be(1); - // 1 Stable - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) - .Should() - .Be(1); - // 1 RC - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) - .Should() - .Be(1); - // 1 Beta - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) - .Should() - .Be(1); - // 1 label per version - productInfo.Versions.Count(item => item.Labels.Count == 1).Should().Be(4); - - // Stable 1.1 - string? stableVersion = productInfo - .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) - ?.Version; - stableVersion.Should().Be("1.1"); - // Latest 2.1 - string? latestVersion = productInfo - .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) - ?.Version; - latestVersion.Should().Be("2.1"); - // RC 3.1 - string? rcVersion = productInfo - .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.RC)) - ?.Version; - rcVersion.Should().Be("3.1"); - // Beta 4.1 - string? betaVersion = productInfo - .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) - ?.Version; - betaVersion.Should().Be("4.1"); - } - - [Fact] - public void VersionRegress() - { - // Create test releases - List<ProductInfo> oldProductList = - [ - new ProductInfo - { - Product = ProductInfo.ProductType.NxMeta, - Versions = - { - new VersionInfo { Version = "1.0", Labels = { VersionInfo.LabelType.Stable } }, - new VersionInfo { Version = "2.0", Labels = { VersionInfo.LabelType.Latest } }, - new VersionInfo { Version = "3.0", Labels = { VersionInfo.LabelType.RC } }, - new VersionInfo { Version = "4.0", Labels = { VersionInfo.LabelType.Beta } }, - }, - }, - ]; - List<ProductInfo> newProductList = - [ - new ProductInfo - { - Product = ProductInfo.ProductType.NxMeta, - Versions = - { - new VersionInfo { Version = "0.9", Labels = { VersionInfo.LabelType.Stable } }, - new VersionInfo { Version = "1.9", Labels = { VersionInfo.LabelType.Latest } }, - new VersionInfo { Version = "2.9", Labels = { VersionInfo.LabelType.RC } }, - new VersionInfo { Version = "3.9", Labels = { VersionInfo.LabelType.Beta } }, - }, - }, - ]; - - // newProductList will be updated in-place - // Only Stable and Latest is tested - // Versions with multiple labels will update the version not the individual labels - ReleaseVersionForward.Verify(oldProductList, newProductList); - ProductInfo productInfo = newProductList.First(); - - // 4 versions - productInfo.Versions.Should().HaveCount(4); - // 1 Latest - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) - .Should() - .Be(1); - // 1 Stable - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) - .Should() - .Be(1); - // 1 RC - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) - .Should() - .Be(1); - // 1 Beta - productInfo - .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) - .Should() - .Be(1); - // 1 label per version - productInfo.Versions.Count(item => item.Labels.Count == 1).Should().Be(4); - - // Stable 1.0 - string? stableVersion = productInfo - .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) - ?.Version; - stableVersion.Should().Be("1.0"); - // Latest 2.0 - string? latestVersion = productInfo - .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) - ?.Version; - latestVersion.Should().Be("2.0"); - // RC 3.0 - string? rcVersion = productInfo - .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.RC)) - ?.Version; - rcVersion.Should().Be("3.0"); - // Beta 4.0 - string? betaVersion = productInfo - .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) - ?.Version; - betaVersion.Should().Be("4.0"); - } - - [Fact] - public void VersionRegress_FoldsOntoExistingVersion() - { - // Reproduces the WisenetWAVE codegen failure: the online "Latest" regressed below a - // version that is already present as "Stable", so restoring the old "Latest" must fold - // onto the existing entry instead of adding a duplicate version row. - List<ProductInfo> oldProductList = - [ - new ProductInfo - { - Product = ProductInfo.ProductType.WisenetWAVE, - Versions = - { - new VersionInfo - { - Version = "6.0.5.41290", - Labels = { VersionInfo.LabelType.Stable }, - }, - new VersionInfo - { - Version = "6.1.2.42921", - Labels = { VersionInfo.LabelType.Latest }, - }, - }, - }, - ]; - List<ProductInfo> newProductList = - [ - new ProductInfo - { - Product = ProductInfo.ProductType.WisenetWAVE, - Versions = - { - new VersionInfo - { - Version = "6.1.1.42624", - Labels = { VersionInfo.LabelType.Latest }, - }, - new VersionInfo - { - Version = "6.1.2.42921", - Labels = { VersionInfo.LabelType.Stable }, - }, - }, - }, - ]; - - ReleaseVersionForward.Verify(oldProductList, newProductList); - ProductInfo productInfo = newProductList.First(); - - // Folded to a single 6.1.2.42921 entry carrying both Stable and Latest - productInfo.Versions.Should().ContainSingle(); - VersionInfo version = productInfo.Versions.First(); - version.Version.Should().Be("6.1.2.42921"); - version - .Labels.Should() - .BeEquivalentTo([VersionInfo.LabelType.Stable, VersionInfo.LabelType.Latest]); - - // No duplicate version numbers - productInfo.Versions.Select(item => item.Version).Should().OnlyHaveUniqueItems(); - productInfo.Invoking(item => item.VerifyNoDuplicateVersions()).Should().NotThrow(); - } - - [Fact] - public void VerifyNoDuplicateVersions_Throws() - { - // Two entries sharing a version number must be rejected - ProductInfo productInfo = new() - { - Product = ProductInfo.ProductType.NxMeta, - Versions = - { - new VersionInfo - { - Version = "6.1.2.42921", - Labels = { VersionInfo.LabelType.Stable }, - }, - new VersionInfo - { - Version = "6.1.2.42921", - Labels = { VersionInfo.LabelType.Latest }, - }, - }, - }; - - productInfo - .Invoking(item => item.VerifyNoDuplicateVersions()) - .Should() - .Throw<InvalidOperationException>(); - } -} +using CreateMatrix; + +namespace CreateMatrixTests; + +public class VersionForwardTests +{ + [Fact] + public void VersionForward() + { + // Create test releases + List<ProductInfo> oldProductList = + [ + new ProductInfo + { + Product = ProductInfo.ProductType.NxMeta, + Versions = + { + new VersionInfo { Version = "1.0", Labels = { VersionInfo.LabelType.Stable } }, + new VersionInfo { Version = "2.0", Labels = { VersionInfo.LabelType.Latest } }, + new VersionInfo { Version = "3.0", Labels = { VersionInfo.LabelType.RC } }, + new VersionInfo { Version = "4.0", Labels = { VersionInfo.LabelType.Beta } }, + }, + }, + ]; + List<ProductInfo> newProductList = + [ + new ProductInfo + { + Product = ProductInfo.ProductType.NxMeta, + Versions = + { + new VersionInfo { Version = "1.1", Labels = { VersionInfo.LabelType.Stable } }, + new VersionInfo { Version = "2.1", Labels = { VersionInfo.LabelType.Latest } }, + new VersionInfo { Version = "3.1", Labels = { VersionInfo.LabelType.RC } }, + new VersionInfo { Version = "4.1", Labels = { VersionInfo.LabelType.Beta } }, + }, + }, + ]; + + // newProductList will be updated in-place + // Only Stable and Latest is tested + // Versions with multiple labels will update the version not the individual labels + ReleaseVersionForward.Verify(oldProductList, newProductList); + ProductInfo productInfo = newProductList.First(); + + // 4 versions + productInfo.Versions.Should().HaveCount(4); + // 1 Latest + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) + .Should() + .Be(1); + // 1 Stable + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) + .Should() + .Be(1); + // 1 RC + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) + .Should() + .Be(1); + // 1 Beta + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) + .Should() + .Be(1); + // 1 label per version + productInfo.Versions.Count(item => item.Labels.Count == 1).Should().Be(4); + + // Stable 1.1 + string? stableVersion = productInfo + .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) + ?.Version; + stableVersion.Should().Be("1.1"); + // Latest 2.1 + string? latestVersion = productInfo + .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) + ?.Version; + latestVersion.Should().Be("2.1"); + // RC 3.1 + string? rcVersion = productInfo + .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.RC)) + ?.Version; + rcVersion.Should().Be("3.1"); + // Beta 4.1 + string? betaVersion = productInfo + .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) + ?.Version; + betaVersion.Should().Be("4.1"); + } + + [Fact] + public void VersionRegress() + { + // Create test releases + List<ProductInfo> oldProductList = + [ + new ProductInfo + { + Product = ProductInfo.ProductType.NxMeta, + Versions = + { + new VersionInfo { Version = "1.0", Labels = { VersionInfo.LabelType.Stable } }, + new VersionInfo { Version = "2.0", Labels = { VersionInfo.LabelType.Latest } }, + new VersionInfo { Version = "3.0", Labels = { VersionInfo.LabelType.RC } }, + new VersionInfo { Version = "4.0", Labels = { VersionInfo.LabelType.Beta } }, + }, + }, + ]; + List<ProductInfo> newProductList = + [ + new ProductInfo + { + Product = ProductInfo.ProductType.NxMeta, + Versions = + { + new VersionInfo { Version = "0.9", Labels = { VersionInfo.LabelType.Stable } }, + new VersionInfo { Version = "1.9", Labels = { VersionInfo.LabelType.Latest } }, + new VersionInfo { Version = "2.9", Labels = { VersionInfo.LabelType.RC } }, + new VersionInfo { Version = "3.9", Labels = { VersionInfo.LabelType.Beta } }, + }, + }, + ]; + + // newProductList will be updated in-place + // Only Stable and Latest is tested + // Versions with multiple labels will update the version not the individual labels + ReleaseVersionForward.Verify(oldProductList, newProductList); + ProductInfo productInfo = newProductList.First(); + + // 4 versions + productInfo.Versions.Should().HaveCount(4); + // 1 Latest + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) + .Should() + .Be(1); + // 1 Stable + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) + .Should() + .Be(1); + // 1 RC + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)) + .Should() + .Be(1); + // 1 Beta + productInfo + .Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) + .Should() + .Be(1); + // 1 label per version + productInfo.Versions.Count(item => item.Labels.Count == 1).Should().Be(4); + + // Stable 1.0 + string? stableVersion = productInfo + .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Stable)) + ?.Version; + stableVersion.Should().Be("1.0"); + // Latest 2.0 + string? latestVersion = productInfo + .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Latest)) + ?.Version; + latestVersion.Should().Be("2.0"); + // RC 3.0 + string? rcVersion = productInfo + .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.RC)) + ?.Version; + rcVersion.Should().Be("3.0"); + // Beta 4.0 + string? betaVersion = productInfo + .Versions.FirstOrDefault(item => item.Labels.Contains(VersionInfo.LabelType.Beta)) + ?.Version; + betaVersion.Should().Be("4.0"); + } + + [Fact] + public void VersionRegress_FoldsOntoExistingVersion() + { + // Reproduces the WisenetWAVE codegen failure: the online "Latest" regressed below a + // version that is already present as "Stable", so restoring the old "Latest" must fold + // onto the existing entry instead of adding a duplicate version row. + List<ProductInfo> oldProductList = + [ + new ProductInfo + { + Product = ProductInfo.ProductType.WisenetWAVE, + Versions = + { + new VersionInfo + { + Version = "6.0.5.41290", + Labels = { VersionInfo.LabelType.Stable }, + }, + new VersionInfo + { + Version = "6.1.2.42921", + Labels = { VersionInfo.LabelType.Latest }, + }, + }, + }, + ]; + List<ProductInfo> newProductList = + [ + new ProductInfo + { + Product = ProductInfo.ProductType.WisenetWAVE, + Versions = + { + new VersionInfo + { + Version = "6.1.1.42624", + Labels = { VersionInfo.LabelType.Latest }, + }, + new VersionInfo + { + Version = "6.1.2.42921", + Labels = { VersionInfo.LabelType.Stable }, + }, + }, + }, + ]; + + ReleaseVersionForward.Verify(oldProductList, newProductList); + ProductInfo productInfo = newProductList.First(); + + // Folded to a single 6.1.2.42921 entry carrying both Stable and Latest + productInfo.Versions.Should().ContainSingle(); + VersionInfo version = productInfo.Versions.First(); + version.Version.Should().Be("6.1.2.42921"); + version + .Labels.Should() + .BeEquivalentTo([VersionInfo.LabelType.Stable, VersionInfo.LabelType.Latest]); + + // No duplicate version numbers + productInfo.Versions.Select(item => item.Version).Should().OnlyHaveUniqueItems(); + productInfo.Invoking(item => item.VerifyNoDuplicateVersions()).Should().NotThrow(); + } + + [Fact] + public void VerifyNoDuplicateVersions_Throws() + { + // Two entries sharing a version number must be rejected + ProductInfo productInfo = new() + { + Product = ProductInfo.ProductType.NxMeta, + Versions = + { + new VersionInfo + { + Version = "6.1.2.42921", + Labels = { VersionInfo.LabelType.Stable }, + }, + new VersionInfo + { + Version = "6.1.2.42921", + Labels = { VersionInfo.LabelType.Latest }, + }, + }, + }; + + productInfo + .Invoking(item => item.VerifyNoDuplicateVersions()) + .Should() + .Throw<InvalidOperationException>(); + } +} diff --git a/CreateMatrixTests/VersionInfoTests.cs b/CreateMatrixTests/VersionInfoTests.cs index 5cb2f32a..884baea7 100644 --- a/CreateMatrixTests/VersionInfoTests.cs +++ b/CreateMatrixTests/VersionInfoTests.cs @@ -1,26 +1,26 @@ -using CreateMatrix; - -namespace CreateMatrixTests; - -public sealed class VersionInfoTests -{ - [Fact] - public void SetVersion_RemovesReleaseSuffix() - { - VersionInfo versionInfo = new(); - - versionInfo.SetVersion("5.1.0.35151 R1"); - - versionInfo.Version.Should().Be("5.1.0.35151"); - } - - [Fact] - public void BuildNumber_ExtractsRevision() - { - VersionInfo versionInfo = new() { Version = "5.0.0.35271" }; - - int buildNumber = versionInfo.GetBuildNumber(); - - buildNumber.Should().Be(35271); - } -} +using CreateMatrix; + +namespace CreateMatrixTests; + +public sealed class VersionInfoTests +{ + [Fact] + public void SetVersion_RemovesReleaseSuffix() + { + VersionInfo versionInfo = new(); + + versionInfo.SetVersion("5.1.0.35151 R1"); + + versionInfo.Version.Should().Be("5.1.0.35151"); + } + + [Fact] + public void BuildNumber_ExtractsRevision() + { + VersionInfo versionInfo = new() { Version = "5.0.0.35271" }; + + int buildNumber = versionInfo.GetBuildNumber(); + + buildNumber.Should().Be(35271); + } +} diff --git a/Docker/DWSpectrum-LSIO.Dockerfile b/Docker/DWSpectrum-LSIO.Dockerfile index 5691619f..75a0072d 100644 --- a/Docker/DWSpectrum-LSIO.Dockerfile +++ b/Docker/DWSpectrum-LSIO.Dockerfile @@ -77,7 +77,7 @@ COPY s6-overlay /etc/s6-overlay EXPOSE 7001 # Create mount points -# Config links will be created at runtime, see LSIO/etc/s6-overlay/s6-rc.d/init-nx-relocate/run +# Config links will be created at runtime, see Docker/s6-overlay/s6-rc.d/init-nx-relocate/run # /opt/${COMPANY_NAME}/mediaserver/etc -> /config/etc # /opt/${COMPANY_NAME}/mediaserver/var -> /config/var # /root/.config/nx_ini links -> /config/ini diff --git a/Docker/NxGo-LSIO.Dockerfile b/Docker/NxGo-LSIO.Dockerfile index 85afc1c4..8703bbc4 100644 --- a/Docker/NxGo-LSIO.Dockerfile +++ b/Docker/NxGo-LSIO.Dockerfile @@ -77,7 +77,7 @@ COPY s6-overlay /etc/s6-overlay EXPOSE 7001 # Create mount points -# Config links will be created at runtime, see LSIO/etc/s6-overlay/s6-rc.d/init-nx-relocate/run +# Config links will be created at runtime, see Docker/s6-overlay/s6-rc.d/init-nx-relocate/run # /opt/${COMPANY_NAME}/mediaserver/etc -> /config/etc # /opt/${COMPANY_NAME}/mediaserver/var -> /config/var # /root/.config/nx_ini links -> /config/ini diff --git a/Docker/NxMeta-LSIO.Dockerfile b/Docker/NxMeta-LSIO.Dockerfile index e575e676..58cd5b40 100644 --- a/Docker/NxMeta-LSIO.Dockerfile +++ b/Docker/NxMeta-LSIO.Dockerfile @@ -77,7 +77,7 @@ COPY s6-overlay /etc/s6-overlay EXPOSE 7001 # Create mount points -# Config links will be created at runtime, see LSIO/etc/s6-overlay/s6-rc.d/init-nx-relocate/run +# Config links will be created at runtime, see Docker/s6-overlay/s6-rc.d/init-nx-relocate/run # /opt/${COMPANY_NAME}/mediaserver/etc -> /config/etc # /opt/${COMPANY_NAME}/mediaserver/var -> /config/var # /root/.config/nx_ini links -> /config/ini diff --git a/Docker/NxWitness-LSIO.Dockerfile b/Docker/NxWitness-LSIO.Dockerfile index 2b10a059..258ec094 100644 --- a/Docker/NxWitness-LSIO.Dockerfile +++ b/Docker/NxWitness-LSIO.Dockerfile @@ -77,7 +77,7 @@ COPY s6-overlay /etc/s6-overlay EXPOSE 7001 # Create mount points -# Config links will be created at runtime, see LSIO/etc/s6-overlay/s6-rc.d/init-nx-relocate/run +# Config links will be created at runtime, see Docker/s6-overlay/s6-rc.d/init-nx-relocate/run # /opt/${COMPANY_NAME}/mediaserver/etc -> /config/etc # /opt/${COMPANY_NAME}/mediaserver/var -> /config/var # /root/.config/nx_ini links -> /config/ini diff --git a/Docker/README.md b/Docker/README.md index 4f74a4f2..9952dd08 100644 --- a/Docker/README.md +++ b/Docker/README.md @@ -1,12 +1,24 @@ -# Docker Projects for Network Optix VMS Products - -This is a project to build and publish docker images for various [Network Optix](https://www.networkoptix.com/) VMS products. - -## License - -Licensed under the [MIT License](https://github.com/ptr727/NxWitness/blob/main/LICENSE) -![GitHub License](https://img.shields.io/github/license/ptr727/NxWitness) - -## Project - -Refer to the [GitHub NxWitness](https://github.com/ptr727/NxWitness/) project for usage and details. +# NxWitness + +Docker images for Network Optix Nx Witness and OEM-branded VMS products. + +## License + +Licensed under the [MIT License][license-link]\ +![GitHub License][license-shield] + +## Project + +Refer to the [GitHub NxWitness][github-link] project for usage and details. + +<!-- Shields --> + +[license-shield]: https://img.shields.io/github/license/ptr727/NxWitness + +<!-- Distribution --> + +[github-link]: https://github.com/ptr727/NxWitness/ + +<!-- External --> + +[license-link]: https://github.com/ptr727/NxWitness/blob/main/LICENSE diff --git a/Docker/WisenetWAVE-LSIO.Dockerfile b/Docker/WisenetWAVE-LSIO.Dockerfile index 099e9df3..6c621b8a 100644 --- a/Docker/WisenetWAVE-LSIO.Dockerfile +++ b/Docker/WisenetWAVE-LSIO.Dockerfile @@ -77,7 +77,7 @@ COPY s6-overlay /etc/s6-overlay EXPOSE 7001 # Create mount points -# Config links will be created at runtime, see LSIO/etc/s6-overlay/s6-rc.d/init-nx-relocate/run +# Config links will be created at runtime, see Docker/s6-overlay/s6-rc.d/init-nx-relocate/run # /opt/${COMPANY_NAME}/mediaserver/etc -> /config/etc # /opt/${COMPANY_NAME}/mediaserver/var -> /config/var # /root/.config/nx_ini links -> /config/ini diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 00000000..1adaccd8 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,308 @@ +# Fleet Governance Rules + +The cross-cutting rules every repo in the fleet follows. [`AGENTS.md`](./AGENTS.md) is the entry point agents read first and maps each task to the section here that governs it, and this file holds the rule text itself. Code style lives in [`CODESTYLE.md`](./CODESTYLE.md) and the CI/CD workflow contract in [`WORKFLOW.md`](./WORKFLOW.md). This repo's own design lives in [`ARCHITECTURE.md`](./ARCHITECTURE.md) and how it is run in [`OPERATIONS.md`](./OPERATIONS.md). + +Read the one section a task needs rather than the whole file. `grep -n '^## ' GOVERNANCE.md` lists them. + +## Foundational Principles + +The specific rules in this file implement a few governing principles. Read these first: they are the reason the branching, release, and versioning rules are shaped the way they are, and every rule below serves one of them. + +- **Distribution respects the user: pull by default, push only where the channel forces it.** Docker images, GitHub Releases, and NuGet/PyPI packages are **pull**: the user decides when to consume them. A few channels are **push**: HACS surfaces a new release to every installed user as a pending update they did not go looking for, and a consumer that vendors from `main` picks up its current state. Because a release can reach users who did not ask for it, releasing is a deliberate act that marks a real functional change, never mechanical churn. This is why a **human merge never auto-publishes**: a release is a deliberate `workflow_dispatch`, or a conditional auto-release when the App merges a code-affecting Dependabot/codegen PR to `main` (Docker also refreshes on a weekly schedule). That rule, the no-op republish guarantee, and maintainer-gated version bumps all hold the same line: a needless release spends the user's attention and, on a push channel, acts on their machine. +- **Both branches stay in sync, so a promotion never needs a back-merge.** Dependabot and codegen target `develop` and `main` in parallel, so neither branch drifts and a `develop -> main` promotion stays a clean forward merge by default. That is exactly what lets the model be **signed, linear, and free of back-merges**: forward sync removes any need to merge `main` back into `develop`, which the rules forbid. If sync is ever broken (a change lands on one branch only, or normalizes a file on one side), restore it forward-only, never back-merge. See "Branching Model". (These auto-publish rules describe `release` repos. **Operational** repos differ, with direct-to-`develop` commits and a dispatch-only release. See "Operational Repositories".) +- **Two version numbers, two jobs.** The 2-digit `major.minor` in `version.json` carries human meaning: the maintainer raises it only for a functional change (feature, behavior or API change, breaking change), at their discretion, while NBGV owns the patch position and always increments with git height, so every build is uniquely versioned with no edit. Human-facing docs name the 2-digit line, and the toolchain guarantees monotonic builds. See "Release Model". +- **Contracts state what, not how, and favor reuse.** [`WORKFLOW.md`](./WORKFLOW.md) fixes required outcomes, not a required implementation, so two repos may satisfy a guarantee with different YAML. Within that freedom, apply good engineering practice: minimize duplication and maximize reuse, which is why the pipeline splits a carried, generic orchestration layer from a repo-owned build layer. + +## Durable Knowledge and Self-Improvement + +- **Durable knowledge lives in the committed docs, not in agent memory.** Anything a future agent must honor (a rule, a contract, a hard-won gotcha, a pattern worth repeating or one to avoid) belongs in a committed governance file (`AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, or a committed backlog such as a `README.md` TODO section). Agent memory does not survive a new session, a new machine, or a new environment, so it holds only environment-specific nuance and in-flight session state, never anything whose loss on reset would matter. A durable lesson left only in memory is lost to the next agent. +- **Keep the governance current as you work.** When work surfaces something durable (a rule worth enforcing, a recurring gotcha, a positive pattern to repeat, a negative one to design out), record it in the governance docs as part of that change, rather than leaving it in a local note or routing around it with a one-off workaround. Where the governing doc is carried from a template this repo cannot edit directly, propose the change upstream instead of only fixing it locally. Governance is not static: it improves by agents folding good patterns in and designing bad ones out. +- **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** Three conditions together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all three (the #1073 incident happened under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. + +This section keeps the full rules and is surfaced at its decision moment by the `agent-conduct` Skill at `.agents/skills/agent-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. + +## Repository Boundaries and Write Safety + +A state-changing GitHub call is the highest-blast-radius thing an agent does here: it runs under the maintainer's identity, so one wrong target writes to another owner's repository as the maintainer, an outward-facing and hard-to-reverse act. These rules bound every write (a git push, an API mutation, a comment, a label, a merge) on any platform, and they bound a write to a checkout on disk as well, since a blanket add or a hard reset in a working tree another task is using destroys work without ever reaching GitHub. Reads are unrestricted, and how far a local read can be trusted is governed under "Verification Discipline" rather than here. The bounds below are on writes. + +- **Write only within the owner of the current project's repository.** Every state-changing call targets this project's `origin` or another repository under the same owner, which is the fleet the maintainer already administers. A broad or logged-in identity is capability, not permission: a token that *can* reach another owner's repository does not authorize writing to it. Writing under a **different owner** needs explicit human permission naming that repository, granted deliberately rather than assumed from a token's reach, and a "harmless test" write is still a write, so there is no probe exception. That boundary is where the harm sits, since the incident this rule exists for was a stray comment on a stranger's repository, not work across the maintainer's own projects. Reads from anywhere are fine. +- **Provider connectors are read-only for fleet work.** Use a provider's GitHub connector for reads where it helps. Perform each GitHub mutation through the documented hub tool, or through authenticated `gh` where no tool owns the operation. This gives Codex, Claude, opencode, and a terminal session one write path with the same checks. It also avoids a connector mutation that predictably lacks repository authorization while the verified `gh` session already has it. A provider-specific instruction may explain how to reach the common path. It never replaces that path with its own mutation surface. +- **Never fabricate, guess, or reuse an identifier passed to a write.** Capture every identifier a state-changing call consumes from a live query in the **same** session. This includes node, numeric, thread, and comment ids. Pass the captured value directly. Do not hand-type an id, recall it from another session, or copy it from documentation or an example. Ids commonly resolve **globally**, so a wrong-but-valid id does not fail. It writes to the wrong target, in someone else's repository. Apply the same rule to an identifier embedded in outward-facing text. Read the complete URL from the live object. Never construct a plausible link from an unverified id. If a query returns no id or URL, stop rather than invent one to proceed. +- **A write is never a probe, and a write's output is never suppressed.** Never fire a state-changing call to see whether it works: decide it should happen, make it happen, and read the result. Never append output-discarding redirection or a force-success tail to a mutation (for example `>/dev/null`, `2>/dev/null`, `&>/dev/null`, `|| true`, `|| :`, `|| echo`), because the write's output is exactly what must be read. A write that appears to fail is **verified, not assumed harmless**, because the operation may have succeeded on the server while the client reported an error, so confirm the actual state before retrying or moving on. The ban targets hiding a *failure*. An ad-hoc call's response is the only signal you get, so `>/dev/null 2>&1`, `|| true`, and `|| echo`, which swallow the error stream or force success, are never acceptable on one. A committed script under `set -e` is a narrow exception: it may send a write's *stdout* to `/dev/null` to drop the success-response noise, because stderr stays visible and a failed write still aborts loudly (the hub's own `repo-config/configure.sh` does exactly this, and a repository reaches it there rather than carrying a copy). The exception is stdout-only suppression inside a reviewed, fail-loud script, never `2>&1` or a force-success tail, and never an ad-hoc command. +- **A refused write is reported, never re-shaped, and the maintainer's say-so does not lift a refusal by the harness.** These are two different permissions and only one of them is the maintainer's to give. When the agent harness refuses a write, the maintainer authorizing it in conversation does not change the outcome, and the identical call is refused again, so a second attempt is not worth making and reading the second refusal as a flake is how an agent starts hunting for another shape of the same request. **That hunt is the failure this rule exists to stop.** Re-expressing a refused `gh` command as a raw `gh api -X POST` reaches the same endpoint with the same identity and the same blast radius, having defeated the one control that stopped it, and it is the more dangerous version because the agent believes it has permission. So a refused write is never re-attempted through a different API surface, a different tool, or a rephrasing, and it is never routed around by the agent writing itself a permission rule, which is self-authorization whatever the maintainer said. Two routes remain, both of them the maintainer's: they add the permission rule themselves, or they run the command themselves. Raise it as a blocked decision naming those two (see "Communicating with the User"), and where the work needs the result rather than the call, say what the agent will verify once the maintainer has run it. **A refusal is also a fact about the contract, not just about the session**: where a required verification can only be performed by a write the agent is refused, the document requiring it says so and names who runs it, since a check that is mandatory and unperformable is quietly dropped and then reported as done. +- **Each task runs in its own checkout, in its own directory, on its own feature branch.** The unit is the task rather than the agent, since one agent moving between two repositories meets the same hazard as two agents sharing one tree, and a rule written per agent permits exactly the case that goes wrong. The commands that cross the boundary are the ordinary ones rather than the reckless ones, and each is correct in isolation: a blanket `git add -A` sweeps another task's uncommitted work into the commit, a `git reset --hard` deletes it, and a branch switch carries it into an unrelated change. The mechanical habit that holds the rule up is that a mutating command takes an absolute path, or a `cd` to one in the same invocation, rather than the working directory it inherited, because a read in the wrong directory is a wasted call and a write there is damage. +- **A task isolates into its own worktree before its first file edit, and a continuation re-isolates.** All new work begins by creating a unique git worktree (or clone) on its own feature branch, based on the branch work starts on for the repository's model per "Branching Model", which is `develop` unless the task is explicitly about `main`-only content. The primary checkout is the maintainer's own surface, so a session launched there isolates before writing rather than after noticing contention, and a session resuming a prior task creates a fresh worktree rather than resuming wherever its branch happens to be checked out, since a branch sitting checked out in a shared tree is exactly how two sessions end up in one checkout. The moment this rule binds is the first file edit, because the commit-time and review-time checks all run after another task's uncommitted work can already be swept. The worktree mechanics, the layout convention, and the cleanup are packaged as the `repo-worktree` Skill at `.agents/skills/repo-worktree/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo, and this section keeps the rule. For Claude Code sessions, the `gh-write-guard` hook now backstops a mechanical subset of this rule directly, denying a mutating git operation (`reset`, `add`, `commit`, and most of the rest, a documented handful of exemptions such as a fast-forward-only pull kept aside) run against a primary checkout rather than a worktree. Every other agent, and everything about isolation a hook cannot see (which checkout a read happens in, whether another task is live in the tree), still relies on this prose alone. The parenthetical "(or clone)" above names `repo-worktree`'s own standalone-clone fallback, used when a linked worktree is unavailable, and that fallback is itself structurally a primary checkout to the hook's own primary-vs-worktree test, so a session using it on Claude Code sets the hook's `GH_WRITE_GUARD_ALLOW_PRIMARY_CHECKOUT` grant for that session, per `repo-worktree`'s own instructions, rather than being silently denied the commits the fallback exists to make. +- **A checkout another task is live in is left rather than shared, and a footprint already left there is undone deliberately.** Two signals say someone else is in the tree, a branch that changes when nothing you did changed it, and an edit of yours reverted with no conflict, and the response to either is to stop rather than to re-apply the edit, which is the instinct and the wrong one. Leaving and cloning your own costs about a minute against an incident that costs the better part of an hour, so it is the cheap move rather than the cautious one. Once you have written there, leaving it alone arrives too late, so save your work aside, restore only the files you touched, verify the tree is clean, delete your branch from that clone, and then say plainly what was touched, since a regenerated report left behind reads as the other task's own and is committed by whoever runs the next blanket add. + +## Representative Data in Agent-Authored Text + +Agent-authored text illustrates with data the agent constructed, never with data it observed in the maintainer's environment. This binds every surface an agent writes: pull request and issue comments, review replies, commit messages, code, tests, fixtures, and docs. Reading real data is unrestricted, and what is bounded is what an agent copies out of the environment into text that is committed or posted. The rule holds for a private repository as much as a public one, since a repository's audience changes with one settings toggle while the text stays exactly where it was written, and it holds where the data is the maintainer's own, since the exposure happens on their behalf before they can weigh it. + +- **Synthetic evidence is the better evidence, not a weaker substitute.** A case constructed to carry the defect demonstrates it exactly and any reader can re-run it, where observed data proves the same thing and can never be reproduced by anyone else. A filename built to contain a newline is a complete proof of a newline-handling defect, and the real directory it was found in adds nothing the proof needed. Reaching for observed data to make a finding more convincing inverts which of the two is the stronger evidence. Where observed data is what revealed the defect, name its shape, meaning the property that triggers the fault, and construct a case that carries that property. +- **The exposure is one-way.** A public comment is fetched, cached, and indexed the moment it posts, so editing it afterwards is mitigation rather than a fix, and the edit leaves the original readable in the comment's edit history to anyone who can read the repository. Text that has already landed is reported to the maintainer rather than quietly rewritten, since the decision on what to do about it, deletion included, is theirs. Do not quote the exposed data again while reporting or investigating it, because a transcript, an issue, or a commit message written about the exposure reproduces it somewhere new. +- **No checker closes this.** A pattern finds an absolute home path or a drive letter, and gating that subset is worth doing as a floor. The exposure this rule exists for was name-shaped, and a name is not pattern-detectable, so a search of the offending text for path-shaped strings returns nothing while the names sit in plain sight. A gate here catches the easy half, and mistaking it for the answer is what stops anyone looking at the other half, which is why this is a judgment an agent applies rather than a check it waits for. + +## Git and Commit Rules + +The fleet's mechanical git rules: default to staging rather than committing, stage by explicit path only and never with a blanket add, commit means commit and push, every commit is signed and carries the committer's own verified GitHub `noreply` identity, never force push, a history rewrite re-identifies only the commits it touches that aren't yours, and destructive git commands run only on explicit instruction. + +This is packaged as the `git-commit-conventions` Skill at +`.agents/skills/git-commit-conventions/SKILL.md` in the hub, not a repo-relative link since that +path is hub-local and not carried into every fleet repo. The summary above sketches the contract. +Read the skill for the full rules. + +## Branching Model + +Two workflow models, set per repo by the registry `workflowModel` field. Most repos are +`release`: squash-only feature branches into `develop`, merge-commit-only `develop -> main` +promotions, forward-only with no back-merges, and two promotion traps worth knowing before the +first one (never delete `develop`, resolve an EOL-only conflict by taking `develop`'s side). +**GitHub's own "default branch" repository setting reads `main`, but `develop` is where work starts and where in-flight content lives**, so a worktree or clone that defaults to "the default branch" lands on `main` and can silently miss content already merged to `develop` but not yet promoted. Branch from `develop`, on either workflow model, unless the task is explicitly about `main`-only content. +**Operational** repos differ substantially (direct-to-`develop`, advisory CI, dispatch-only +release), covered as a delta rather than a separate model. + +This is packaged as the `operational-vs-release-workflow` Skill at +`.agents/skills/operational-vs-release-workflow/SKILL.md` in the hub, not a repo-relative link +since that path is hub-local and not carried into every fleet repo. The summary above sketches +the contract. Read the skill for the full rules, including branch protection configuration, the +dual-target bot wiring, and the operational-repo delta in full. + +## Release Model + +The **two-phase model is the default**: PRs build fast, publishing is batched, a human merge +never auto-publishes on its own. See [`WORKFLOW.md`](./WORKFLOW.md) for the full CI/CD contract. +Publishing fires on a manual dispatch, a code-affecting bot push to `main`, or (Docker only) a +weekly schedule, and versioning is semantic and maintainer-controlled (NBGV owns the build number, +the maintainer owns the `major.minor` floor). **Operational** repos differ, with a dispatch-only +release and no auto-publish bots. See "Operational Repositories" below. + +This is packaged as part of the `operational-vs-release-workflow` Skill at +`.agents/skills/operational-vs-release-workflow/SKILL.md` in the hub, not a repo-relative link +since that path is hub-local and not carried into every fleet repo. The summary above sketches +the contract. Read the skill for the full rules, including the release-target build layer, the +no-op republish guarantee, and wrapper-repo upstream-version tracking. + +## Operational Repositories + +The registry `workflowModel` field is `release` (the default) or `operational`. **Operational** +repos track a live service's running state rather than shipping versioned units of delivery +(live-service config such as Home Assistant, ESPHome, Vantage, and home automation): commits go +directly to `develop`, CI runs on the push as advisory feedback only, a PR still exists for a +change worth reviewing, the `main` promotion gate is unchanged, and release happens only by manual +dispatch. + +This is packaged as part of the `operational-vs-release-workflow` Skill at +`.agents/skills/operational-vs-release-workflow/SKILL.md` in the hub, not a repo-relative link +since that path is hub-local and not carried into every fleet repo. The summary above sketches +the contract. Read the skill for the full rules, including when a config change still earns a +pull request. + +Line-ending governance for an operational repo is in [Line Endings](#line-endings), where its `[*]` default follows the consuming app's native platform per the registry `lineEndings` field, not the fleet LF default. + +### Repo-Scoped Secrets + +A repo whose own stacks or scripts read local runtime credentials from disk, most commonly an operational repo's Docker Compose stack, keeps them in a dotted `.secrets/` directory at the repo root. This is the repo-scoped counterpart to the host-scoped `~/.secrets/` convention a repo's own `OPERATIONS.md` may document, and it is a different thing from `spec/secrets.json`, the CI/GitHub Actions secret-name registry `spec/audit.py` cross-checks. `spec/secrets.json` governs what a workflow reads from GitHub Actions. This convention governs what a repo's own process reads from its own checkout. + +- **The directory is named `.secrets/`, dotted, never a bare `secrets/`.** +- **A single opaque credential file carries no extension** (`homeassistant_db_password`, not `homeassistant_db_password.txt`), the same reason `README` and `LICENSE` carry none. It is a security property, read but never sourced, not a formatting preference. +- **A structured credential keeps its format's extension** (`.json` for structured config). +- **The shared env file is named for what it configures**, not a bare `.env` (`docker.env` for a repo whose stacks are Docker Compose), so a second env-shaped file added later stays unambiguous. +- **Every real secret file has a tracked `<name>.example` beside it**, and only the `.example` files plus a `README.md` catalog are un-ignored: + + ```gitignore + **/.secrets/* + !**/.secrets/*.example + !**/.secrets/README.md + ``` + + A fresh checkout then documents its own required shape without ever exposing a real value. This negation keeps a real secret file out of a **new** commit. It does not remove one already tracked: `.gitignore` has no effect on a path git already follows. A real secret file found tracked is removed from the index (`git rm --cached <path>`) and its credential is rotated, not just added to `.gitignore` going forward. +- **`.secrets/README.md` is a catalog**, one row per file naming what it holds and what consumes it, plus a short note on how the directory relates to `~/.secrets/` where the repo also touches that. + +## Hub-Hosted Tooling + +The fleet's tooling lives in the hub once and a repository runs it from there rather than holding a copy. A carried script is current only until the next fix to it, and a repository that misses the sweep does not fail loudly, it audits itself with an older gate while reporting the same command in its output. Removing the copy removes the sweep, the stale-copy detection, and the disposition each stale copy earns, all at once. The hub is the repository [`AGENTS.md`](./AGENTS.md) "Fleet Bootstrap" names, and that section is the entry point whenever nothing else present says where it is. + +**What a repository carries and what it reaches is decided by what the content is.** It carries the content it is audited against and the configuration that describes it, meaning its rule text, the files the manifest declares, and the ruleset payloads its own audit diffs the live configuration against. It reaches machinery whose content is identical in every repository, meaning the prose and repository gates, the review digest, and the configuration script, because a file holding no per-repo content is a copy whose only future is to go stale. A tool named in a carried rule is therefore named as the hub's, since the alternative is a pointer to a path the reader does not have, and a pointer that resolves nowhere teaches the reader that a pointer in carried text is decorative. + +**Reaching it is a checkout of the hub rather than a copy of one file.** A tool reads the payloads, tables, and sibling modules beside it, so a single file lifted out of the tree runs against whatever the caller happens to have, which is the copy problem again in a shorter loop. Read `main`, the promoted and gated state, and fetch immediately before running, because a clone is whatever it last fetched rather than the branch it names, and a stale clone answers confidently instead of failing. Name the tool by its path in that checkout and name the target explicitly, since a tool that defaults to the current directory or the current repository resolves somewhere either way, and a result computed against the wrong repository is well-formed. Which directory the command runs in is the tool's own contract rather than a rule here, so a gate reading a working tree runs in the repository under test while a tool taking its target as an argument runs anywhere and is given one. What the rule forbids is letting a default decide which repository the answer is about. + +**A loader is outside this section rather than exempt from it.** The rule above governs a tool that reads hub content, because a tool reads the payloads, tables, and sibling modules beside it. A loader reads none of them: it obtains a tree and hands control to a tool inside that tree, on a host that cannot yet obtain one. The bound is what it may contain rather than who runs it, and it is one line: a loader references no path inside the tree it fetches except the single entry point it hands control to, and everything else it touches is the machine or the network. A loader that grows a second path into that tree has become a tool and is governed above. + +**A report or finding a hub tool produces names the hub commit it ran from.** The tool moves independently of the repository it measures, so a verdict carrying no hub commit cannot be re-run, and two runs that disagree cannot be attributed to the tree or to the tool. The obligation is the runner's rather than the tool's, since a tool reports on the repository it measures rather than on itself, so the commit is read from the hub checkout and written into the report beside the verdict. This is the same requirement "Verification Discipline" places on any claim that gets acted on. + +**CI reaches the same tooling as a pinned action or reusable workflow.** A runner holds no hub checkout, so a workflow consumes the hub's composite action or reusable workflow and pins it to a commit SHA, per the action-pinning rule under "Workflow YAML Conventions". A standard workflow whose job graph is identical across repos of a type is reached the same way, as a `workflow_call` task the hub hosts once, and the repository carries only the caller stub and a composite-action hook for what is genuinely its own. The pin is what makes a released repository's gate reproducible, since an unpinned consume lets a later hub commit fail a re-run of a change that already passed. Branch-dependent behavior belongs inside the consumed action, because `uses:` takes no expressions and a per-branch ref therefore cannot be selected in the workflow file. + +**An unreachable hub means the tool did not run, and that is the result reported.** A carried copy still works offline and a reached one does not, which is the cost this model trades away and the reason to state the failure rather than route around it. A check that cannot run reports itself as not run, never as clean, which is the silent-narrowing failure "Verification Discipline" names. A hand-rolled substitute is not the tool either: a reconstructed gate encodes its author's reading of the rule rather than the rule, agrees with no other repository, and is the duplicated effort this model exists to end, so an agent that cannot reach the hub says so and stops. + +## Pull Request Title and Commit Message Conventions + +A PR title and a commit message share one contract: an imperative subject, 72 characters or fewer, no trailing period, no vague titles like `update stuff` or `wip` (Dependabot's `Bump X from Y to Z` is fine as-is), no unsolicited `Co-Authored-By:` lines, and no release-bump magnitude in the title, since Nerdbank.GitVersioning computes the next version from `version.json` and git history. + +This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. The summary above sketches the contract. Read the skill for the full format, rules, and examples. + +## Documentation Style Conventions + +The fleet's prose and formatting contract, applied to docs and code/workflow comments alike. It governs what a carried file may reference, Markdown link, heading, and tense structure, and the comment philosophy. It also holds the ASCII character-set tiers, the line-ending policy, the sentence-structure house style, and the rule keeping a quantitative claim honest. + +This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. The summary above sketches the contract. Read the skill for the full rules. + +### Comments + +The full comment philosophy, what earns one, structure, capitalization, growth discipline, is in the `comment-and-doc-style` Skill referenced above. + +### Character Set + +The full ASCII tier system (never legitimate, legitimate next to a number, always legitimate, developer-typed Unicode) and the semicolon and spaced-hyphen rules are in the `comment-and-doc-style` Skill referenced above. + +### Line Endings + +The full CRLF/LF policy (`.editorconfig` and `.gitattributes` defaults and pins, choosing an ending for a new file type, operational-repo overrides, editing discipline, and auditing) is in the `comment-and-doc-style` Skill referenced above. + +### Sentence Structure + +ASD-STE100's structural half is the adopted house style: short sentences, one instruction per sentence, active voice, and imperative mood for procedure steps. Its controlled dictionary is deliberately not adopted. The full rules, the sentence word cap, and the opt-in `sentence-length` check that enforces the cap are in the `comment-and-doc-style` Skill referenced above. + +## Verification Discipline + +The checks that separate work actually done from work that merely reports success. Their unifying property: **every failure below is green.** A skipped job and a passing job are indistinguishable in the aggregated required check, a pattern that matches less still exits zero, and a gate that stops gating still reports success. No linter, status check, or review layer catches any of them. + +- **Locate every check a change owes before running any of them, and CI's coverage is not that list.** The checks are read from what the repository declares, meaning its [`OPERATIONS.md`](./OPERATIONS.md) "Local Verification" section alongside the workflows, rather than inferred from whatever the pipeline happens to run. Part of a repository's contract is routinely unreachable from a runner, a redirect no build serves, a deploy no pull request performs, hardware no runner holds, so the check covering that part lives in a document rather than in a workflow and is run by hand before the pull request opens. Green is then the precise signal that it was skipped, because the pipeline reports success over the half it reaches while saying nothing about the half it cannot. Reading a document's own description of itself is not how such a check is found, since a topical document is named for its most visible function, usually a post-merge one, and an accurate description of that function routes a pre-merge task away from the file holding the gate. The destination is declared fleet-wide for that reason, so finding the check never depends on how well one repository worded a pointer to it. +- **A test runner failing to spawn is not evidence that no test coverage applies here.** `uv run pytest` failing to spawn in a lint-only Python Scripts profile is that profile working as intended, not a missing dependency, per the `python-codestyle` Skill's Two Profiles. Read the actual invocation from the same `OPERATIONS.md` "Local Verification" section the bullet above names, rather than guessing a generic test-runner command, and report that document's own command result, not the guessed command's failure. +- **A test must assert the mechanism it names, and a gate has to be watched failing.** Label each case by the behavior it proves, then write the case that reintroduces the fault and confirm the gate objects to it. A case that passes for an incidental reason, the right answer reached by the wrong path, is worse than no case, because it is later cited as evidence. A proof that restates the gated data instead of reading it proves only that the function works, so drive the real table or the real config. And a gate that finds nothing is indistinguishable from a gate with nothing to find, so assert a floor on what a healthy run covers. +- **Gates, filters, and gate-like watchers fail loud, never narrow quietly.** A pattern that silently matches less, an allowlist that silently stops matching, or a gate that silently stops gating all report success while doing nothing. When a construct exists to notice something, make the not-noticing case produce an error or an annotation. An identity allowlist used as a gate, for one, must raise an error when its list stops matching, not silently pass everything through. +- **Config with a uniqueness rule is validated on read, and its consumers assert what it promised.** A repeated key in a lookup table is not a precedence question to settle quietly, it is two answers to one question, and keeping whichever came last picks one of them where the reader sees no choice being made. Fail on the duplicate at the point the config is read, so the code downstream can rely on the invariant instead of re-deriving it. +- **Validate and read on the same normalized key.** A guard that compares stripped names while the join looks up the raw one passes a padded key and then matches nothing, so the exact fault the guard exists to stop is sitting inside the guard. Normalize once at the boundary and use that one value for both the check and the lookup. +- **Run the repo's whole lint gate before every push, not the parts that look relevant.** CI runs all of them, so a partial local run only defers the failure, and the tool most likely to catch a given change is often the one it seems least about (an edit that manipulates line endings is exactly when `editorconfig-checker` matters). The repo documents each linter's known-working invocation, and this rule is that **all** of them run. +- **Editing CRLF files programmatically: `.` matches `\r` in a regex**, so a captured line keeps its carriage return and rejoining with `\r\n` yields `CRCRLF`. A text-mode rewrite has the mirror failure, silently flattening CRLF to LF. Prefer line-based edits (`splitlines(keepends=True)`) or literal replacement over regex reassembly. In Python the text-mode failure is the *default*: `Path.read_text()` decodes through universal newlines and `write_text()` writes `\n` back, so a read-edit-write round trip flattens the whole file while the edit itself looks correct. Pass `newline=''` to both, or work in bytes. This is the mechanism behind the Line Endings warning above, and it is worth naming because the corruption is invisible in a rendered diff. +- **Scope a check by what the project declares, not by the file that prompted it.** A check written while editing one file tends to cover that file's language and stop, and then reports success on every other surface the rule governs. Read the declared types, or the config that enumerates them, and cover each one, then assert a floor per surface so a table that narrows fails loudly instead of passing quietly. A rule about comments means every comment syntax the project ships, and a format that carries comments in practice counts even where its specification says otherwise. +- **Never edit source through a shell heredoc when the text carries backslash escapes.** The shell consumes the escape and writes an invisible control character in its place, so a `\b` inside a regex becomes a backspace and the pattern silently matches nothing while every test still passes. Use a file-editing tool for such text. When a check inspects text for control characters, use `str.isprintable()` rather than a codepoint floor, since DEL and the Unicode format characters sit above 32 and are equally invisible in a diff. +- **Never edit an active `.code-workspace` file.** A workspace file rewritten on disk can make VS Code reload the window, and a reload destroys the running agent session's context, so the work in flight is lost with nothing to catch it, and the trigger is not fully characterized (an agent's edit has caused the reload where a human's identical edit did not). Surface the needed change for the maintainer to apply by hand. +- **A green check is not evidence the work happened.** A skipped job and a passing job are indistinguishable in the aggregated required check. When a job exists to exercise something, confirm from its log that it ran and produced the output it promises. +- **A local clone is not the branch it names, it is whatever that clone last fetched.** Reading a checkout on disk answers what that clone last saw, so a finding taken from one carries a date nobody stated, and two failures of exactly that shape are on record from one session: a repository reported as still drifted on a file whose fix had already merged, and a repository reported as missing a file it carries because the checkout sat on an older branch. Read the live ref through the API where the claim will be acted on, or fetch immediately before reading, and name the ref and the commit in any finding a local read produced. A clone stays the right tool for anything needing history or a build, which an API read cannot give. +- **A checkout already sitting on disk is not yours to trust for being there.** A clone or worktree this session did not create, found while looking around a machine, may belong to another concurrent session's task, sit on a stale fetch or a branch nobody expects, or hold uncommitted edits nobody has reviewed, and none of that is visible from the directory listing that found it. Running `git status`, `git remote -v`, or `git branch --show-current` against it, or reading a file inside it, answers for whatever that checkout happens to hold at that moment, not for the repository, and the found checkout is not the "local clone" the bullet above means, since this session never fetched it and has no basis for trusting what it last saw. Clone the repository fresh into a location this session controls, or read the live state through the GitHub API, rather than adopting a pre-existing checkout as ground truth. +- **A "does not exist" claim names the branch it was checked against.** A worktree or checkout answers for whichever ref it was built from, and that ref is not necessarily the one the content lives on: a `release`-model repo carries in-flight content on `develop`, per "Branching Model" above, well before it reaches `main`, so a worktree defaulted to the fleet's default branch can hold nothing while the repository holds everything. Before reporting a file, a directory, or a piece of content as absent anywhere in a repo, check it against the branch the repo's own model designates as current for that kind of content, not only whichever branch a worktree or checkout happened to default to, and name the branch the negative claim was checked against in the finding itself. +- **A raw-file fetch 404s the same way for a private repository as for a genuinely missing file.** `curl`ing `raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>` returns an indistinguishable 404 whether the repository is private, the ref does not exist, or the path is wrong, so an agent that treats that response as "the content does not exist" has made the same unstated-branch mistake the bullet above names, only over visibility instead of branch. Where a repository's visibility is not confirmed public, read its content through `gh api "repos/<owner>/<repo>/contents/<path>?ref=<ref>"` instead. Capture the result before decoding it rather than piping straight into `base64 -d`: `content=$(gh api "repos/<owner>/<repo>/contents/<path>?ref=<ref>" --jq '.content') && printf '%s' "$content" | base64 -d`. A direct pipe reports only the decoder's own exit status, so a failed `gh api` call (a bad ref, an auth gap, a network error) can hand `base64 -d` nothing to decode, which it does successfully, misreading a failed fetch as a successful empty read. The `&&` form fails on the fetch itself instead of decoding past it. Never merge the error stream into either form with `2>&1`: a failed call's error text flows into `base64 -d` in place of content and corrupts the decode with no clue why. Verify the ref resolves (a commit SHA is unambiguous where a branch name may have moved, been deleted, or never existed on the remote) before reading either failure as an answer about the content itself. +- **A launched process is not a result, and a cause nobody observed is not a diagnosis.** "The watcher is armed" names a process rather than a finding, so what gets reported is the output that process produced, and where it produced none, that absence is the report. The failure it prevents is an agent standing still on a condition that was met half an hour earlier, having announced the wait and never read it. Naming an external cause for such a stall afterwards, a throttle or a quota that appears nowhere in the record, turns a local defect into a story about someone else and closes the investigation on the wrong party, so read the record for the cause before naming one, and where the record does not carry it, report the cause as unknown. +- **A workflow change is only fully exercised by CI.** Extracting a `run:` block and executing it locally validates the script and nothing else, because `secrets: inherit`, `permissions:`, `needs:` wiring, and reusable-workflow inputs resolve only in a real run. +- **Platform-specific code is "verified" only on the platform it runs on.** PowerShell on Windows, a macOS-only `mktemp`/`ssh-agent` behavior, a WSL-specific path quirk: an agent reasoning about such code from a different host, however carefully, has not executed it, and reasoning by structural analogy to an already-tested equivalent on another platform ("the POSIX version works, so the PowerShell version should too") is a plausible first pass, not verification. State it as exactly that, an unverified structural match, and never in the same words used for a tested fact. When no agent in the loop has access to the target platform, say so, and either defer the platform-specific portion to a human or an agent that has that access, or ship it clearly labeled unverified. +- **A review flags an instance, so fix the class.** When a reviewer cites one stale claim, one silent-narrowing pattern, or one mis-worded contract, sweep for its siblings before replying. Reviewers sample rather than enumerate. + +This section keeps the full rules and is surfaced at its decision moment by the `agent-conduct` Skill at `.agents/skills/agent-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. + +## PR Review Etiquette + +The provider-agnostic review-loop contract every fleet repo follows starts when a pull request opens. Open every fleet-owned pull request ready for review. Draft state is reserved for the separately documented upstream contribution workflow while a third-party contribution is still being prepared. Creating the pull request is not a terminal handoff. Run the review status once in the foreground. Then start the bounded review wait in a background process. Request a review on every push. Confirm it covers the current head SHA and the full diff rather than only part of it. Triage every finding, including low-confidence findings collapsed into the review body rather than threads. Reply to and resolve every addressed finding. Repeat after every fix until the checks are green and the current-head review leaves no finding open. Only an explicit maintainer instruction may stop, defer, or alter this default. Silence or a request that says only "open a PR" is not such an instruction. Never merge on a green or CLEAN merge state alone. That state does not prove the review covered the current head SHA and full diff. It also does not expose unanswered low-confidence findings that opened no thread. + +This is packaged as the `pr-review-conduct` Skill at `.agents/skills/pr-review-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. The summary above sketches the contract. Read the skill for the merge gate, the expected loop, and how a finding is closed. + +The provider-specific mechanics this contract needs to actually drive GitHub Copilot, how to request a review, poll for it, match the suppressed-findings heading, verify coverage, and reply or resolve a thread without a hand-typed id, live in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) "GitHub Copilot Review Runbook", unaffected by this pointer. That content is read directly by the Copilot reviewer bot, which cannot read a Skill, so it stays fully written out rather than being packaged the same way. + +## Communicating with the User + +- **Reference every pull request as a clickable link.** When you mention a PR on a surface that renders Markdown (chat, a summary, a report), render it as a Markdown link to the PR (`[#123](https://github.com/OWNER/REPO/pull/123)`), never a bare `#123`. The same applies to issues and commits. **The form follows the surface.** Some surfaces link neither a Markdown link nor a bare URL, an interactive prompt's question and option text among them, and pasting a full URL into one of those does not rescue it, since the reader gets a string to copy, which is the outcome this rule exists to prevent. There the reference is a bare `#123`, and the clickable link goes in the message that comes **before** the prompt rather than merely alongside it, because the prompt blocks on an answer and a message emitted after it is read once that answer is already given, which is the one moment the link is no longer any use. The test is whether the reader can click it where it is read, not whether it was written in the syntax that works elsewhere. +- **Ask for input as a numbered list.** When you need the user to decide or answer, present the questions, and any options, as a numbered list so they can reply per number. A single inline question is fine, and two or more are always numbered. +- **Raise work blocked on the user as a direct interactive prompt.** When progress needs a decision, an authorization, or an answer only the user can give, ask for it through the interface's own prompt mechanism, at the point the work stops. Never leave it as prose in a summary: a handoff buried in a paragraph is a handoff that did not happen, because a summary reads as a report of finished work and the one line still waiting on the user is the easiest in it to skim past. The blocked item is the message, not a closing remark on a message about something else. **The options offered are the actions themselves**, and the one that unblocks the work names the action it authorizes ("squash and merge it"), so selecting it is the go-ahead rather than a note to act on later. Offering only ways to wait is the same failure in interactive clothing, since a prompt whose every choice is inaction reports the block rather than clearing it, and where the agent may not perform the authorized action itself, the option says who does it. This supersedes the numbered-list rule above wherever an interactive prompt is available, and the numbered list is the fallback where none is. + +This section keeps the full rules and is surfaced at its decision moment by the `agent-conduct` Skill at `.agents/skills/agent-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. + +## Workflow YAML Conventions + +These conventions describe the target state. New and modified workflows must respect them. The rest of the repo is expected to be brought up to the same standard. Sweep PRs that apply a rule everywhere are welcome when a rule changes. + +This section and [`WORKFLOW.md`](./WORKFLOW.md) keep the full rules, this section winning where the two overlap, and both are surfaced by the `workflow-ci-contract` Skill at `.agents/skills/workflow-ci-contract/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. + +- **Action pinning**: pin **every** action, first-party (`actions/*`) and third-party alike, to a commit SHA with a trailing `# vX.Y.Z` comment, so Renovate / Dependabot can still bump it but a tag swap can't change the executed code. Use `# vX` (major-only) only when the upstream's floating major tag doesn't correspond to a specific patch/minor release SHA, since pinning to the floating-tag SHA still gives the SHA guarantee, the version comment just records the major line. Documented exception (no SHA pin at all): `dotnet/nbgv` is consumed via `@master` because the upstream tag stream lags `master` substantially and Dependabot's tag-tracking would propose a downgrade. **This applies to repo-owned build-layer leaves too**, since a leaf owning its build specifics is not a reason to use floating tags, and Dependabot still bumps SHA pins (updating the SHA + version comment). +- **Filename**: reusable workflows (those with `on: workflow_call`) end in `-task.yml`. Entry-point workflows (`on: push` / `pull_request` / `schedule` / `workflow_dispatch`) do NOT use the `-task` suffix. They end with what they do: `-pull-request.yml`, `-release.yml`, etc. The suffix carries semantic meaning: a `-task.yml` file is meant to be `uses:`-d, never triggered directly. +- **Workflow `name:`** (the top-level `name:` field): reusable workflow names end in **"task"** (e.g. `Build PyPI library task`), and entry-point workflow names end in **"action"** (e.g. `Publish project release action`, `Test pull request action`). The displayed action name in the GitHub Actions UI tells you at a glance whether you're looking at an orchestrator or a callee. +- **Job and step `name:` suffixes**: every job's `name:` ends in **"job"** and every step's `name:` ends in **"step"**, including the PR-gate aggregator, whose `name:` is a required-status-check `context:` in a branch ruleset (`Check pull request workflow status job` in `test-pull-request.yml`). A ruleset-bound job's `name:` and its ruleset `context:` are the **same string**: rename them **together**, updating the live ruleset and the hub's `repo-config/` payloads in lockstep with the job `name:`, never one without the other, or required-status-check enforcement silently breaks. There is no un-suffixed exception. +- **Concurrency**: top-level workflows declare `concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true }` so a fresh push supersedes an in-flight run on the same ref. **Documented exceptions** (both record the rationale inline in their header comment): (1) [`merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml) uses `cancel-in-progress: false` because the merge-bot's job model (enable-auto-merge on opened, disable-auto-merge on maintainer-pushed synchronize, with method dispatched by base) requires each event to run to completion in arrival order, because cancellation would leave auto-merge in an inconsistent state. (2) `.github/workflows/publish-release.yml` uses both a **global, ref-independent group** (`group: ${{ github.workflow }}`, dropping the usual `-${{ github.ref }}`) and `cancel-in-progress: false`. It publishes shared ref-independent artifacts (both branches' Docker tags/caches and GitHub releases) on schedule/dispatch regardless of the triggering ref, so a ref-scoped group would let a scheduled run (ref `main`) and a manual dispatch (ref `develop`) run concurrently and double-push, and cancelling a publish mid-flight can leave a partially pushed tag set or a half-created release. The global group + queueing serializes every publish run to completion. +- **Shells**: every bash surface, a multi-line `run:` block and every committed `.sh` script alike, starts with `set -Eeuo pipefail`: fail fast, fail on undefined vars, fail on a failed pipe segment, and let an `ERR` trap inherit into functions, subshells, and command substitutions (`-E`). The `-E` is defense in depth: the fleet ships no `ERR` trap today, so a script that later adds one inherits the behavior instead of silently losing it. +- **Conditionals**: multi-line `if:` uses folded scalar `if: >-` so YAML preserves whitespace correctly. Literal block (`if: |`) is wrong because it embeds newlines inside the boolean expression. +- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks, since one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans, and `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms: `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`. +- **Validate input/state consistency at entry, fail fast**: when a workflow's inputs must satisfy a cross-input or input-versus-derived-state invariant (e.g. the release branch must match the computed version's prerelease status, or two inputs are mutually exclusive), assert it **once** in a dedicated entry validation step/job that the downstream jobs `needs:`, before any expensive build or publish work, not as partial checks scattered deep in later jobs. One gate that fails fast with a clear `::error::` beats a late or one-directional check. Examples: `build-release-task.yml`'s `validate-release` job (branch-versus-prerelease, both directions) and `publish-docker-readme-task.yml`'s "Validate inputs step". +- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit. +- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies, since `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`. +- **Artifact retention**: workflow artifacts are an intra-run handoff only, with durable copies living on the GitHub release rather than in workflow artifacts, so they must not survive the run and accumulate against the small account-wide artifact-storage quota. **Clean up each transfer artifact surgically at its point of consumption**: the job that downloads it deletes it by exact name/pattern right after consuming it (the `github-release` job deletes `release-asset-<branch>-*` after attaching them to the release, and `publish-release.yml`'s `publish-pypi` deletes `pypi-build-<branch>` after publishing). Deletion needs `actions: write` granted on that job, and for a reusable callee (e.g. `github-release` inside `build-release-task.yml`) the **caller** grants it (`publish-release.yml`'s `publish` job does). **Never blanket-delete the run's artifacts** (`gh api .../artifacts --jq '.artifacts[].id'`). That also destroys diagnostic/log artifacts and the build-records actions emit automatically (`docker/build-push-action`'s `.dockerbuild`), which are exactly what you need to debug a failed run. Set `retention-days: 1` on **every** explicit `upload-artifact`: it is the failure-path backstop, since a job that dies before its consumer runs leaves its artifact to be reaped within a day, so no separate terminal cleanup job is needed. A repo customizing these jobs must preserve the consume-then-delete shape. +- **Docker layer cache**: cache to/from a registry tag (`type=registry`, e.g. `buildcache-<branch>` on Docker Hub), not the GitHub Actions cache (`type=gha`), to keep large image layers off the 10 GB Actions cache. A **multi-image** repo uses a **per-image** buildcache tag (`<repo>:buildcache-<branch>` for each image, plus the base image's own tag and inline cache). It does not fall back to `type=gha` for the extra images. +- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly, because without it GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (which may differ from the exact commit NBGV versioned) and not a branch name (a moving ref that a mid-run commit could advance past the built tree). + +## Running the Linters Locally (Known-Working Invocations) + +CI runs the full lint set, but run the linters locally before pushing to catch issues early, so an agent must know how to invoke them. Their non-Docker install paths (curl-pipe installers, global npm) are frequently blocked in sandboxes or fail on WSL, so **prefer the Docker invocations below, the known-working path that needs no local toolchain.** These tools auto-discover their targets from the working directory. None of markdownlint, cspell, actionlint, editorconfig-checker, shellcheck, shfmt, or PSScriptAnalyzer is installed natively on a fleet host, by decision, so a `command -v <tool>` miss means exactly that and not that the check is unavailable here: read this section for the working invocation before reporting a check as skipped or unavailable. + +**Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): + +- **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check), **shellcheck** and **shfmt** the same way for a repo that carries a shell script, and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (none of the three has an action). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. A "shell script" here is a tracked `.sh` file, plus a tracked, extension-less file whose shebang names bash or sh, the shape a script meant to run as a bare command takes: a bare `*.sh` glob misses that second case, so both discovery paths run before either tool does. This whole block is the hub's `validate-task.yml` reusable workflow, so a fleet repo reaches it rather than carrying a copy of these steps. +- **A working local hook is strongly suggested fleet-wide, and its absence is a measured audit finding, not an invisible gap.** `spec/project-types.json`'s `parity.hooks` check reads this section for its rationale, judged by hand during an `AUDIT.md` run like every sibling check in its dimension, never mechanized by `spec/audit.py`: a repo with no local hook mechanism wired at all is a `linter-parity` defect, the same severity a missing markdownlint config already gets, while a repo mid-convergence (below) stays operational. CI remains the authoritative run regardless. Two catalog snippets carry the canonical shape, `catalog/snippets/husky/` (Husky.Net, for .NET or any project including Python) and `catalog/snippets/pre-commit/` (the Python `pre-commit` framework, for a repo with no `.husky/` tree), each carrying a copy of `catalog/snippets/hub-fetch-run.py` alongside it. +- **The hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff and the repo's type checker for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to the working tree diff against `HEAD` rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. That scope is the working tree rather than the staged index, so a partially staged file is judged on all of its edits, not only the staged ones, and CI re-checks the whole tree regardless, which is what makes that scope affordable in a hook. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out of the hook regardless, since it resolves a same-owner pin against the GitHub API. +- **The doc gates reach a downstream repo by fetching `.github/actions/prose-gate/prose_lint.py` and `.github/actions/repo-gate/repo_gate.py` fresh from `ptr727/ProjectTemplate`'s `main` branch, via `hub-fetch-run.py`, never vendored and never pinned.** Pinning anything Dependabot does not maintain goes stale by construction, and CI (this repo's own, and the hub's) is the backstop that catches a change landing broken on `main` before a locally fetched copy does real damage. These are the only network calls the doc gates make, one per fetched script. A Python repo's `uvx`-run ruff and type checker can also reach the network, to resolve `@latest` on a cache miss or refresh, the same category of dependency as the Docker pulls the VS Code Lint tasks already do routinely, not a new one. A fetch failure fails the commit, and it never silently skips the gate. The **hub's own** `.husky/pre-commit` is the one exception, staying local and offline, since it already carries `scripts/prose_lint.py` and `scripts/repo_gate.py` directly and has no hub to reach. A repo enables its hook per clone with `git config core.hooksPath .husky` or (`uv tool install pre-commit` once, then) `pre-commit install`. The Husky.Net snippet needs one more step per clone, `dotnet tool restore` then `dotnet husky install`, which generates `.husky/_/husky.sh`, the file the hook sources. CI remains the authoritative run either way. +- **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks, plus `Lint: Prose` and `Lint: EOL`, the same two hook gates in whole-repo mode rather than diff-scoped, for on-demand full-tree validation. + +The Docker invocations below run the same tools and configs as the VS Code tasks. Their headless form separates the image pull and minimizes repository exposure for an agent executor. + +**Restricted executors keep tool state in a task-specific writable temporary directory.** Set each tool's own cache variable, such as `UV_CACHE_DIR` and `RUFF_CACHE_DIR`, instead of changing `HOME` or an agent configuration directory. A sandbox denial is not a lint result. Preserve the denial, then rerun the required command through the executor's scoped approval mechanism. Network approval covers any required fetch, including an image or package download. Host approval covers access to the Docker socket. Repository-exposure approval covers letting third-party image code read the checkout, even through a read-only mount. Persist approval only when the executor constrains the read-only mount, disabled networking, and resolved digest together. Never allow an unconstrained `docker run` prefix. PSScriptAnalyzer's separate module-install phase gets network approval without any repository mount. Report the approved rerun as the evidence. + +Agent-specific authorization stays separate from the executor-neutral contract above: + +- **Codex:** execution rules match exact argument prefixes, so they cannot safely cover changing worktree paths and digests. Smart Approvals can therefore request repository-exposure approval per task. The no-prompt alternative combines `sandbox_mode = "danger-full-access"` with `approval_policy = "never"`. Use that pair only when an external sandbox contains the Codex process. It removes protection from every command rather than only lint. + +Run the hub-hosted wrapper from the repository it checks: + +```sh +python3 /path/to/ProjectTemplate/scripts/docker_lint.py --root "$PWD" +``` + +The wrapper discovers tracked and unignored targets before it pulls applicable images. It reports a zero-target skip without pulling or mounting the repository. It pulls each applicable image in a distinct pull phase, then resolves the pulled repository digest. A digest prevents the tag from changing between the pull and execution. It does not make third-party code trusted. + +After all pulls, the wrapper reports that repository mounts are about to begin. Each execution uses the resolved digest, disabled networking, and a read-only checkout mount. PSScriptAnalyzer installs its pinned module in a separate container without the checkout mount. File-argument linters receive each tracked path as a distinct argument, split across bounded batches before host command-line limits become relevant. + +Every primary Docker command has a five-minute timeout by default. Use `--timeout` to select another positive bound. The wrapper emits a start and completion line for each primary command. Timeout cleanup has a separate maximum of 30 seconds and emits its result through the failed lint step. The wrapper reports the checked-file count for every linter, including tools that produce no success output. Timeout, container failure, zero-target execution, and successful quiet completion have distinct result lines. The wrapper names each lint container and removes it after a timeout. + +Use repeated `--linter` options for a subset. The supported names are `editorconfig-checker`, `actionlint`, `markdownlint`, `cspell`, `shellcheck`, `shfmt`, and `PSScriptAnalyzer`. editorconfig-checker reads the mounted tree. actionlint reads eligible workflows and includes shellcheck for `run:` blocks. markdownlint reads tracked and unignored Markdown files. CSpell reads `README.md` and `HISTORY.md` only. shellcheck, shfmt, and PSScriptAnalyzer run only when matching scripts are tracked or unignored. shellcheck and shfmt share one target list, a tracked `.sh` file plus a tracked, extension-less file whose shebang names bash or sh. + +In a configured editor the `DavidAnson.vscode-markdownlint` extension is enough for Markdown. Use the wrapper for a headless run or before pushing. + +When pulling a public image fails on a Docker-Desktop/WSL credential-helper error (`docker-credential-desktop.exe: exec format error`), retry with an empty Docker config: `DOCKER_CONFIG=$(mktemp -d) docker run ...` after writing `{}` to `$DOCKER_CONFIG/config.json`. + +## Supported Development Platforms + +- **Cross-platform by default: Windows + macOS + Linux.** Linux runs natively (a Linux desktop, or SSH/remote into a Linux host), through a devcontainer on Windows or macOS, or through WSL2 on Windows, where the devcontainer and WSL routes carry their own nuances (mounts, path translation, SSH-agent forwarding) but deliver the same toolchain. Editing is cross-platform through the GUI regardless of where code runs. Assume this default. +- **A repo's platform ceiling is set by its dependencies, not by tooling effort, so decide it per repo before writing dev tooling.** Narrow below the default only for a hard runtime ceiling, where the code can only execute or test on one platform (e.g. a Home Assistant integration is Linux-only: HA Core has POSIX-only dependencies and will not run natively on Windows, so even maximal tooling yields only lint-only there). The narrowing axis is where code *executes* for dev and testing (native, SSH-remote, container, or CI), never where editing happens. +- **Record a narrowed platform and its reason in the repo** (README/AGENTS) so the restriction reads as a deliberate dependency ceiling, not an omission. + +## Devcontainer + +Contributors commit to this repo with signed commits. This repo ships no devcontainer definition: the toolchain it needs is the .NET SDK plus Docker, both of which the fleet host contract already provides, so a container would add a layer without removing a prerequisite. Build and debug run against the host SDK through the VS Code tasks in `.vscode/tasks.json`, and the image builds run against the host Docker daemon. + +## Editor and Tasks + +- **VS Code is the primary IDE, and the experience favors it.** Prefer VS Code tasks and launch configurations for building, running, and testing over ad-hoc shell scripts. A script is the fallback, not the default. +- The `.code-workspace` file carries the shared editor settings and the recommended-extension set. **All VS Code settings and extension recommendations live only here, never in a standalone `.vscode/settings.json` or `.vscode/extensions.json`** (`.vscode/` holds only `tasks.json` and `launch.json`). A **standard set** of extensions applies to every repo (markdownlint, cspell, editorconfig, markdown-all-in-one, better-todo-tree, github-actions, actionlint, shellcheck, claude-code); **language-specific** extensions are added per project (.NET: csdevkit, csharpier; Python: python, pylance, ruff, mypy; Docker: the Docker extension). +- The Table of Contents is maintained by the Markdown All in One extension, and `markdown.extension.toc.levels` in the workspace sets which heading levels it includes (see the Markdown rules for the authoring convention and the `<!-- omit from toc -->` exclusion marker). +- **Agents: editing the active `.code-workspace` can reload the VS Code window and drop the agent's session.** Commit all state first, prefer opening the folder rather than the workspace while editing it, or leave workspace edits to the maintainer (a maintainer edit does not reload). + +## Repository Details + +Every repo's GitHub repository details (the About panel) follow a fixed convention so the fleet stays consistent and self-describing. + +- **Description** is one canonical sentence that carries to the README, the About panel, and (for a Docker repo) the Docker Hub short description alike, at most **100 characters**, Docker Hub's short-description cap and the tightest surface it feeds. Once a repo declares `registry/repos.json`'s optional `description` field, that field is the source, itself link-free plain text on one line for the same reason: `repo-config/configure.sh apply` writes it to the About panel directly, and the README's **tagline** (its first non-empty line after the `#` H1 heading) follows it rather than the other way around. A repo that has not adopted the field yet keeps the pre-existing convention, where the README tagline is the source of truth and the About panel is set from it by hand (`gh api -X PATCH repos/<owner>/<repo> -f description=...`). `spec/audit.py`'s `description_findings()` reports drift either way, falling back to the tagline when no field is declared. It is that one line and not the paragraph it opens: a README may carry further paragraphs below the tagline, and no mirror reads them. When the current description is *more specific* than the declared source (a chip revision or variant it omits), surface the drift to the maintainer rather than silently discarding the detail, and the fix is to sharpen the declared source so the other mirrors follow it. Docker Hub receives it from the About panel, which the docker-readme task reads at publish time, so an About panel left diverged from the canonical value is carried onward rather than corrected there. +- **Topics** are optional, and any that are present match the repo's actual content. Do not invent topics to fill the field. +- **Include in the home page**: Releases on, Deployments off, Packages off. These toggles are UI-only, since the REST and GraphQL APIs neither read nor write them, so they are set by hand and cannot be audited through `gh`. + +## Repository Layout + +- [`AGENTS.md`](./AGENTS.md): the agent entry point, carrying context and delegation rules plus the map to the sections below. +- [`CLAUDE.md`](./CLAUDE.md): imports `AGENTS.md`, since Claude Code reads `CLAUDE.md` and never `AGENTS.md` on its own. Carries no rule of its own. +- [`GOVERNANCE.md`](./GOVERNANCE.md), [`CODESTYLE.md`](./CODESTYLE.md), [`WORKFLOW.md`](./WORKFLOW.md): the governance docs. This file is the cross-cutting-rules authority. +- [`ARCHITECTURE.md`](./ARCHITECTURE.md): how this repo is built, its module layout, its data flow, and the deviations it deliberately keeps from the fleet template. +- [`OPERATIONS.md`](./OPERATIONS.md): how this repo is run, verified locally, and debugged. +- [`CreateMatrix/`](./CreateMatrix/): the codegen console app that reads upstream product versions and writes this repo's Dockerfiles, Compose files, and build matrix. Not a shipped package. +- [`CreateMatrixTests/`](./CreateMatrixTests/): its xUnit test project. +- [`Docker/`](./Docker/): the generated per-product Dockerfiles, the hand-written base images they derive from, and the s6-overlay service tree. +- [`Make/`](./Make/): the codegen inputs and outputs (`Version.json`, `Matrix.json`) plus the generated Compose files and the scripts that drive them. +- [`Samples/`](./Samples/): captured upstream API responses used as test fixtures. +- [`Unraid/`](./Unraid/): the Unraid community-application templates. +- [`.github/workflows/`](./.github/workflows/): the validation, smoke-build, codegen, and publish workflows. +- [`.github/skills/`](./.github/skills/): the fleet's agent skills, carried whole and never hand-edited. + +After editing a doc, run the linters (see "Running the Linters Locally") before commit. The Comments, Character Set, and Line Endings rules above are the frequent regressions, so hold them exactly. diff --git a/HISTORY.md b/HISTORY.md index 91036fe0..3e13af74 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,63 +1,64 @@ -# Docker Projects for Network Optix VMS Products - -This is a project to build and publish docker images for various [Network Optix][networkoptix-link] VMS products. - -## Release History - -- Version 2.14: - - Fixed an LSIO permissions race where the startup `chown` could run before the `PUID`/`PGID` remap, leaving `/config` and `/media` owned by the wrong user and breaking media writes. -- Version 2.13: - - Fixed a regression bug that surfaced when Nx released an older version under the same tag, triggering the version-forward-release only logic. -- Version 2.12: - - Reworked the CI pipeline: pull requests run a fast representative amd64 smoke build (NxMeta and NxMeta-LSIO) instead of the full matrix, publishing moved to a weekly schedule (and manual trigger) that builds both the `main` and `develop` branches in one run, and merges no longer republish images. This speeds up PR feedback, reduces GH Actions usage, and stops no-op image updates for consumers. -- Version 2.11: - - Add `currentOsVariantOverride=docker` to `mediaserver.conf` following the pattern used in the current Nx [Dockerfile][nxvmsdockerfile-link], and documented in this [support][nxpackage-link] article. -- Version 2.10: - - Split the docker images into a base image and product images, improves build times and reduces GH actions cache utilization. -- Version 2.9: - - Refactoring to match layout and style used in other projects. - - Updated to .NET 10, adding support for Nullable, and AOT. - - Restructured CLI handling logic. - - Update GHA cache usage to prevent the GH actions cache from running out of space. -- Version 2.8: - - Add default `analytics` volume, along with `media` and `backup` volumes, media server complains when sharing analytics storage. -- Version 2.7: - - Modified version update checking actions to create a PR vs. committing changes directly. -- Version 2.6: - - Updated to Ubuntu Noble 24.04 LTS base images in [support][nxossupport-link] of v6 products. - - Update `CreateMatrix` tool to use .NET 9 and stricter `dotnet format` using `.editorconfig`. -- Version 2.5: - - Added [NxGo][nxgo-link] builds, a version of Nx Witness targeted at the transportation sector, [PR][nxgopr-link] by @kinnairdclan, thank you. -- Version 2.4: - - Added [Hanwha Vision][hanwhavision-link] [Wisenet WAVE VMS][hanwhawave-link] builds, another US OEM whitelabel version Nx Witness. - - Using the `CreateMatrix` utility instead of M4 to create Docker and Compose files for all product variants. -- Version 2.3: - - Added unit test project to verify the release and upgrade control logic. - - Switched from `Newtonsoft.Json` to .NET native `Text.Json`. - - Modified builds to account for v6 Beta installers requiring the `file` package but not listing it in DEB `Depends`, see [#142][filedepends-link]. -- Version 2.2: - - Simplified `Dockerfile` creation by using shell scripts instead of a `Makefile`. -- Version 2.1: - - Added ARM64 images per user [request][arm64request-link]. - - Note that testing was limited to verifying that the containers run on a Raspberry Pi 5. - - Updated build scripts to use `docker compose` (vs. `docker-compose`) and `docker buildx` (vs. `docker build`) per current Docker/Moby v25+ [release][dockerinstall-link]. - - Updated `CreateMatrix` tooling to use the newest version for the `latest` tag when multiple versions are available. -- Version 2.0: - - Added a build release [version][version-link], this version is independent of Nx release versions, and only identifies the version of the build environment, and is used in the image label. - - Nx released v5.1 across all product brands, v5.1 [supports][nxossupport-link] Ubuntu Jammy 22.04 LTS, and all base images have been updated to Jammy. - - Due to the Jammy dependency versions older than v5.1 are no longer being built. - - Build scripts removed support for old v4 variants. - - Added a link from `/root/.config/nx_ini` to `/config/ini` for additional INI configuration files. - -[arm64request-link]: https://github.com/ptr727/NxWitness/issues/131 -[dockerinstall-link]: https://docs.docker.com/engine/install/ -[filedepends-link]: https://github.com/ptr727/NxWitness/issues/142 -[hanwhavision-link]: https://hanwhavisionamerica.com/ -[hanwhawave-link]: https://wavevms.com/ -[networkoptix-link]: https://www.networkoptix.com/ -[nxgo-link]: https://www.networkoptix.com/nx-go -[nxgopr-link]: https://github.com/ptr727/NxWitness/pull/172 -[nxossupport-link]: https://support.networkoptix.com/hc/en-us/articles/205313168-Nx-Witness-Operating-System-Support -[nxpackage-link]: https://support.networkoptix.com/hc/en-us/articles/32917149024535-Creating-Update-Packages-for-Custom-Linux-and-or-ARM-Servers -[nxvmsdockerfile-link]: https://github.com/networkoptix/nxvms-docker/blob/master/Dockerfile -[version-link]: ./version.json +# NxWitness + +Docker images for Network Optix Nx Witness and OEM-branded VMS products. + +## Release History + +- Version 2.15: + - Build, CI, and repository tooling changes only. No functional change to the published images. +- Version 2.14: + - Fixed an LSIO permissions race where the startup `chown` could run before the `PUID`/`PGID` remap, leaving `/config` and `/media` owned by the wrong user and breaking media writes. +- Version 2.13: + - Fixed a regression bug that surfaced when Nx released an older version under the same tag, triggering the version-forward-release-only logic. +- Version 2.12: + - Reworked the CI pipeline: pull requests run a fast representative amd64 smoke build (NxMeta and NxMeta-LSIO) instead of the full matrix, publishing moved to a weekly schedule (and manual trigger) that builds both the `main` and `develop` branches in one run, and merges no longer republish images. This speeds up PR feedback, reduces GH Actions usage, and stops no-op image updates for consumers. +- Version 2.11: + - Add `currentOsVariantOverride=docker` to `mediaserver.conf` following the pattern used in the current Nx [Dockerfile][nxvmsdockerfile-link], and documented in this [support][nxpackage-link] article. +- Version 2.10: + - Split the docker images into a base image and product images, improves build times and reduces GH actions cache utilization. +- Version 2.9: + - Refactoring to match layout and style used in other projects. + - Updated to .NET 10, adding support for Nullable, and AOT. + - Restructured CLI handling logic. + - Update GHA cache usage to prevent the GH actions cache from running out of space. +- Version 2.8: + - Add default `analytics` volume, along with `media` and `backup` volumes, media server complains when sharing analytics storage. +- Version 2.7: + - Modified version update checking actions to create a PR vs. committing changes directly. +- Version 2.6: + - Updated to Ubuntu Noble 24.04 LTS base images in [support][nxossupport-link] of v6 products. + - Update `CreateMatrix` tool to use .NET 9 and stricter `dotnet format` using `.editorconfig`. +- Version 2.5: + - Added [NxGo][nxgo-link] builds, a version of Nx Witness targeted at the transportation sector, [PR][nxgopr-link] by @kinnairdclan, thank you. +- Version 2.4: + - Added [Hanwha Vision][hanwhavision-link] [Wisenet WAVE VMS][hanwhawave-link] builds, another US OEM whitelabel version Nx Witness. + - Using the `CreateMatrix` utility instead of M4 to create Docker and Compose files for all product variants. +- Version 2.3: + - Added unit test project to verify the release and upgrade control logic. + - Switched from `Newtonsoft.Json` to .NET native `Text.Json`. + - Modified builds to account for v6 Beta installers requiring the `file` package but not listing it in DEB `Depends`, see [#142][filedepends-link]. +- Version 2.2: + - Simplified `Dockerfile` creation by using shell scripts instead of a `Makefile`. +- Version 2.1: + - Added ARM64 images per user [request][arm64request-link]. + - Note that testing was limited to verifying that the containers run on a Raspberry Pi 5. + - Updated build scripts to use `docker compose` (vs. `docker-compose`) and `docker buildx` (vs. `docker build`) per current Docker/Moby v25+ [release][dockerinstall-link]. + - Updated `CreateMatrix` tooling to use the newest version for the `latest` tag when multiple versions are available. +- Version 2.0: + - Added a build release [version][version-link], this version is independent of Nx release versions, and only identifies the version of the build environment, and is used in the image label. + - Nx released v5.1 across all product brands, v5.1 [supports][nxossupport-link] Ubuntu Jammy 22.04 LTS, and all base images have been updated to Jammy. + - Due to the Jammy dependency versions older than v5.1 are no longer being built. + - Build scripts removed support for old v4 variants. + - Added a link from `/root/.config/nx_ini` to `/config/ini` for additional INI configuration files. + +[arm64request-link]: https://github.com/ptr727/NxWitness/issues/131 +[dockerinstall-link]: https://docs.docker.com/engine/install/ +[filedepends-link]: https://github.com/ptr727/NxWitness/issues/142 +[hanwhavision-link]: https://hanwhavisionamerica.com/ +[hanwhawave-link]: https://wavevms.com/ +[nxgo-link]: https://www.networkoptix.com/nx-go +[nxgopr-link]: https://github.com/ptr727/NxWitness/pull/172 +[nxossupport-link]: https://support.networkoptix.com/hc/en-us/articles/205313168-Nx-Witness-Operating-System-Support +[nxpackage-link]: https://support.networkoptix.com/hc/en-us/articles/32917149024535-Creating-Update-Packages-for-Custom-Linux-and-or-ARM-Servers +[nxvmsdockerfile-link]: https://github.com/networkoptix/nxvms-docker/blob/master/Dockerfile +[version-link]: ./version.json diff --git a/LICENSE b/LICENSE index c07453d5..51539ce9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2026 Pieter Viljoen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2026 Pieter Viljoen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Make/Matrix.json b/Make/Matrix.json index bcead07e..0f7a7f40 100644 --- a/Make/Matrix.json +++ b/Make/Matrix.json @@ -1,605 +1,605 @@ -{ - "SchemaVersion": 2, - "Images": [ - { - "Name": "NxGo", - "Product": "nxgo", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxgo:6.1.2.42921", - "docker.io/ptr727/nxgo:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxGo", - "Product": "nxgo", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxgo:6.1.3.43301", - "docker.io/ptr727/nxgo:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxGo", - "Product": "nxgo", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxgo:develop-6.1.2.42921", - "docker.io/ptr727/nxgo:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxGo", - "Product": "nxgo", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxgo:develop", - "docker.io/ptr727/nxgo:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxGo-LSIO", - "Product": "nxgo", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxgo-lsio:6.1.2.42921", - "docker.io/ptr727/nxgo-lsio:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxGo-LSIO", - "Product": "nxgo", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxgo-lsio:6.1.3.43301", - "docker.io/ptr727/nxgo-lsio:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxGo-LSIO", - "Product": "nxgo", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxgo-lsio:develop-6.1.2.42921", - "docker.io/ptr727/nxgo-lsio:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxGo-LSIO", - "Product": "nxgo", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxgo-lsio:develop", - "docker.io/ptr727/nxgo-lsio:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxMeta", - "Product": "nxmeta", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxmeta:6.1.2.42921", - "docker.io/ptr727/nxmeta:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxMeta", - "Product": "nxmeta", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxmeta:6.1.3.43301", - "docker.io/ptr727/nxmeta:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxMeta", - "Product": "nxmeta", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxmeta:develop-6.1.2.42921", - "docker.io/ptr727/nxmeta:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxMeta", - "Product": "nxmeta", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxmeta:develop", - "docker.io/ptr727/nxmeta:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxMeta-LSIO", - "Product": "nxmeta", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxmeta-lsio:6.1.2.42921", - "docker.io/ptr727/nxmeta-lsio:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxMeta-LSIO", - "Product": "nxmeta", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxmeta-lsio:6.1.3.43301", - "docker.io/ptr727/nxmeta-lsio:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxMeta-LSIO", - "Product": "nxmeta", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxmeta-lsio:develop-6.1.2.42921", - "docker.io/ptr727/nxmeta-lsio:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxMeta-LSIO", - "Product": "nxmeta", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxmeta-lsio:develop", - "docker.io/ptr727/nxmeta-lsio:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxWitness", - "Product": "nxwitness", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxwitness:6.1.2.42921", - "docker.io/ptr727/nxwitness:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxWitness", - "Product": "nxwitness", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxwitness:6.1.3.43301", - "docker.io/ptr727/nxwitness:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxWitness", - "Product": "nxwitness", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxwitness:develop-6.1.2.42921", - "docker.io/ptr727/nxwitness:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxWitness", - "Product": "nxwitness", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxwitness:develop", - "docker.io/ptr727/nxwitness:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxWitness-LSIO", - "Product": "nxwitness", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxwitness-lsio:6.1.2.42921", - "docker.io/ptr727/nxwitness-lsio:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxWitness-LSIO", - "Product": "nxwitness", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxwitness-lsio:6.1.3.43301", - "docker.io/ptr727/nxwitness-lsio:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "NxWitness-LSIO", - "Product": "nxwitness", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxwitness-lsio:develop-6.1.2.42921", - "docker.io/ptr727/nxwitness-lsio:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "NxWitness-LSIO", - "Product": "nxwitness", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxwitness-lsio:develop", - "docker.io/ptr727/nxwitness-lsio:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "DWSpectrum", - "Product": "dwspectrum", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/dwspectrum:6.1.1.42624", - "docker.io/ptr727/dwspectrum:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" - ] - }, - { - "Name": "DWSpectrum", - "Product": "dwspectrum", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/dwspectrum:6.1.3.43301", - "docker.io/ptr727/dwspectrum:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "DWSpectrum", - "Product": "dwspectrum", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/dwspectrum:develop-6.1.1.42624", - "docker.io/ptr727/dwspectrum:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" - ] - }, - { - "Name": "DWSpectrum", - "Product": "dwspectrum", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/dwspectrum:develop", - "docker.io/ptr727/dwspectrum:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "DWSpectrum-LSIO", - "Product": "dwspectrum", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/dwspectrum-lsio:6.1.1.42624", - "docker.io/ptr727/dwspectrum-lsio:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" - ] - }, - { - "Name": "DWSpectrum-LSIO", - "Product": "dwspectrum", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/dwspectrum-lsio:6.1.3.43301", - "docker.io/ptr727/dwspectrum-lsio:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "DWSpectrum-LSIO", - "Product": "dwspectrum", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/dwspectrum-lsio:develop-6.1.1.42624", - "docker.io/ptr727/dwspectrum-lsio:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" - ] - }, - { - "Name": "DWSpectrum-LSIO", - "Product": "dwspectrum", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/dwspectrum-lsio:develop", - "docker.io/ptr727/dwspectrum-lsio:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "WisenetWAVE", - "Product": "wisenetwave", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/wisenetwave:6.1.2.42921", - "docker.io/ptr727/wisenetwave:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "WisenetWAVE", - "Product": "wisenetwave", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/wisenetwave:6.1.3.43301", - "docker.io/ptr727/wisenetwave:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "WisenetWAVE", - "Product": "wisenetwave", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/wisenetwave:develop-6.1.2.42921", - "docker.io/ptr727/wisenetwave:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "WisenetWAVE", - "Product": "wisenetwave", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/wisenetwave:develop", - "docker.io/ptr727/wisenetwave:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "WisenetWAVE-LSIO", - "Product": "wisenetwave", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/wisenetwave-lsio:6.1.2.42921", - "docker.io/ptr727/wisenetwave-lsio:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "WisenetWAVE-LSIO", - "Product": "wisenetwave", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/wisenetwave-lsio:6.1.3.43301", - "docker.io/ptr727/wisenetwave-lsio:latest" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip" - ] - }, - { - "Name": "WisenetWAVE-LSIO", - "Product": "wisenetwave", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/wisenetwave-lsio:develop-6.1.2.42921", - "docker.io/ptr727/wisenetwave-lsio:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip" - ] - }, - { - "Name": "WisenetWAVE-LSIO", - "Product": "wisenetwave", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/wisenetwave-lsio:develop", - "docker.io/ptr727/wisenetwave-lsio:develop-6.1.3.43301" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.3.43301", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip" - ] - } - ] +{ + "SchemaVersion": 2, + "Images": [ + { + "Name": "NxGo", + "Product": "nxgo", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxgo:6.1.2.42921", + "docker.io/ptr727/nxgo:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxGo", + "Product": "nxgo", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxgo:6.1.3.43301", + "docker.io/ptr727/nxgo:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxGo", + "Product": "nxgo", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxgo:develop-6.1.2.42921", + "docker.io/ptr727/nxgo:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxGo", + "Product": "nxgo", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxgo:develop", + "docker.io/ptr727/nxgo:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxGo-LSIO", + "Product": "nxgo", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxgo-lsio:6.1.2.42921", + "docker.io/ptr727/nxgo-lsio:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxGo-LSIO", + "Product": "nxgo", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxgo-lsio:6.1.3.43301", + "docker.io/ptr727/nxgo-lsio:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxGo-LSIO", + "Product": "nxgo", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxgo-lsio:develop-6.1.2.42921", + "docker.io/ptr727/nxgo-lsio:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxGo-LSIO", + "Product": "nxgo", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxgo-lsio:develop", + "docker.io/ptr727/nxgo-lsio:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxMeta", + "Product": "nxmeta", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxmeta:6.1.2.42921", + "docker.io/ptr727/nxmeta:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxMeta", + "Product": "nxmeta", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxmeta:6.1.3.43301", + "docker.io/ptr727/nxmeta:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxMeta", + "Product": "nxmeta", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxmeta:develop-6.1.2.42921", + "docker.io/ptr727/nxmeta:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxMeta", + "Product": "nxmeta", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxmeta:develop", + "docker.io/ptr727/nxmeta:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxMeta-LSIO", + "Product": "nxmeta", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxmeta-lsio:6.1.2.42921", + "docker.io/ptr727/nxmeta-lsio:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxMeta-LSIO", + "Product": "nxmeta", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxmeta-lsio:6.1.3.43301", + "docker.io/ptr727/nxmeta-lsio:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxMeta-LSIO", + "Product": "nxmeta", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxmeta-lsio:develop-6.1.2.42921", + "docker.io/ptr727/nxmeta-lsio:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxMeta-LSIO", + "Product": "nxmeta", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxmeta-lsio:develop", + "docker.io/ptr727/nxmeta-lsio:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxWitness", + "Product": "nxwitness", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxwitness:6.1.2.42921", + "docker.io/ptr727/nxwitness:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxWitness", + "Product": "nxwitness", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxwitness:6.1.3.43301", + "docker.io/ptr727/nxwitness:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxWitness", + "Product": "nxwitness", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxwitness:develop-6.1.2.42921", + "docker.io/ptr727/nxwitness:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxWitness", + "Product": "nxwitness", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/nxwitness:develop", + "docker.io/ptr727/nxwitness:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxWitness-LSIO", + "Product": "nxwitness", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxwitness-lsio:6.1.2.42921", + "docker.io/ptr727/nxwitness-lsio:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxWitness-LSIO", + "Product": "nxwitness", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxwitness-lsio:6.1.3.43301", + "docker.io/ptr727/nxwitness-lsio:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "NxWitness-LSIO", + "Product": "nxwitness", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxwitness-lsio:develop-6.1.2.42921", + "docker.io/ptr727/nxwitness-lsio:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxWitness-LSIO", + "Product": "nxwitness", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxwitness-lsio:develop", + "docker.io/ptr727/nxwitness-lsio:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "DWSpectrum", + "Product": "dwspectrum", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/dwspectrum:6.1.1.42624", + "docker.io/ptr727/dwspectrum:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42624", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" + ] + }, + { + "Name": "DWSpectrum", + "Product": "dwspectrum", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/dwspectrum:6.1.3.43301", + "docker.io/ptr727/dwspectrum:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "DWSpectrum", + "Product": "dwspectrum", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/dwspectrum:develop-6.1.1.42624", + "docker.io/ptr727/dwspectrum:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42624", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" + ] + }, + { + "Name": "DWSpectrum", + "Product": "dwspectrum", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/dwspectrum:develop", + "docker.io/ptr727/dwspectrum:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "DWSpectrum-LSIO", + "Product": "dwspectrum", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/dwspectrum-lsio:6.1.1.42624", + "docker.io/ptr727/dwspectrum-lsio:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42624", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" + ] + }, + { + "Name": "DWSpectrum-LSIO", + "Product": "dwspectrum", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/dwspectrum-lsio:6.1.3.43301", + "docker.io/ptr727/dwspectrum-lsio:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "DWSpectrum-LSIO", + "Product": "dwspectrum", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/dwspectrum-lsio:develop-6.1.1.42624", + "docker.io/ptr727/dwspectrum-lsio:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42624", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" + ] + }, + { + "Name": "DWSpectrum-LSIO", + "Product": "dwspectrum", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/dwspectrum-lsio:develop", + "docker.io/ptr727/dwspectrum-lsio:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "WisenetWAVE", + "Product": "wisenetwave", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/wisenetwave:6.1.2.42921", + "docker.io/ptr727/wisenetwave:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "WisenetWAVE", + "Product": "wisenetwave", + "Branch": "main", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/wisenetwave:6.1.3.43301", + "docker.io/ptr727/wisenetwave:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "WisenetWAVE", + "Product": "wisenetwave", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/wisenetwave:develop-6.1.2.42921", + "docker.io/ptr727/wisenetwave:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "WisenetWAVE", + "Product": "wisenetwave", + "Branch": "develop", + "Base": "ubuntu", + "Tags": [ + "docker.io/ptr727/wisenetwave:develop", + "docker.io/ptr727/wisenetwave:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "WisenetWAVE-LSIO", + "Product": "wisenetwave", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/wisenetwave-lsio:6.1.2.42921", + "docker.io/ptr727/wisenetwave-lsio:stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "WisenetWAVE-LSIO", + "Product": "wisenetwave", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/wisenetwave-lsio:6.1.3.43301", + "docker.io/ptr727/wisenetwave-lsio:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip" + ] + }, + { + "Name": "WisenetWAVE-LSIO", + "Product": "wisenetwave", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/wisenetwave-lsio:develop-6.1.2.42921", + "docker.io/ptr727/wisenetwave-lsio:develop-stable" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "WisenetWAVE-LSIO", + "Product": "wisenetwave", + "Branch": "develop", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/wisenetwave-lsio:develop", + "docker.io/ptr727/wisenetwave-lsio:develop-6.1.3.43301" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.3.43301", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip" + ] + } + ] } \ No newline at end of file diff --git a/Make/Test-develop.yml b/Make/Test-develop.yml index 86dd9c9d..13cf131e 100644 --- a/Make/Test-develop.yml +++ b/Make/Test-develop.yml @@ -1,258 +1,256 @@ -# Compose file created by CreateMatrix, do not modify by hand - -volumes: - - # Dockerfile : NxGo - test_nxgo_etc: - test_nxgo_ini: - test_nxgo_var: - test_nxgo_media: - test_nxgo_backup: - test_nxgo_analytics: - - # Dockerfile : NxGo-LSIO - test_nxgo-lsio_config: - test_nxgo-lsio_media: - test_nxgo-lsio_backup: - test_nxgo-lsio_analytics: - - # Dockerfile : NxMeta - test_nxmeta_etc: - test_nxmeta_ini: - test_nxmeta_var: - test_nxmeta_media: - test_nxmeta_backup: - test_nxmeta_analytics: - - # Dockerfile : NxMeta-LSIO - test_nxmeta-lsio_config: - test_nxmeta-lsio_media: - test_nxmeta-lsio_backup: - test_nxmeta-lsio_analytics: - - # Dockerfile : NxWitness - test_nxwitness_etc: - test_nxwitness_ini: - test_nxwitness_var: - test_nxwitness_media: - test_nxwitness_backup: - test_nxwitness_analytics: - - # Dockerfile : NxWitness-LSIO - test_nxwitness-lsio_config: - test_nxwitness-lsio_media: - test_nxwitness-lsio_backup: - test_nxwitness-lsio_analytics: - - # Dockerfile : DWSpectrum - test_dwspectrum_etc: - test_dwspectrum_ini: - test_dwspectrum_var: - test_dwspectrum_media: - test_dwspectrum_backup: - test_dwspectrum_analytics: - - # Dockerfile : DWSpectrum-LSIO - test_dwspectrum-lsio_config: - test_dwspectrum-lsio_media: - test_dwspectrum-lsio_backup: - test_dwspectrum-lsio_analytics: - - # Dockerfile : WisenetWAVE - test_wisenetwave_etc: - test_wisenetwave_ini: - test_wisenetwave_var: - test_wisenetwave_media: - test_wisenetwave_backup: - test_wisenetwave_analytics: - - # Dockerfile : WisenetWAVE-LSIO - test_wisenetwave-lsio_config: - test_wisenetwave-lsio_media: - test_wisenetwave-lsio_backup: - test_wisenetwave-lsio_analytics: - - -services: - - # Dockerfile : NxGo - # Port : 7101 - nxgo: - image: docker.io/ptr727/nxgo:develop - container_name: nxgo-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7101:7001 - volumes: - - test_nxgo_etc:/opt/networkoptix/mediaserver/etc - - test_nxgo_ini:/home/networkoptix/.config/nx_ini - - test_nxgo_var:/opt/networkoptix/mediaserver/var - - test_nxgo_media:/media - - test_nxgo_backup:/backup - - test_nxgo_analytics:/analytics - - # Dockerfile : NxGo-LSIO - # Port : 7201 - nxgo-lsio: - image: docker.io/ptr727/nxgo-lsio:develop - container_name: nxgo-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7201:7001 - volumes: - - test_nxgo-lsio_config:/config - - test_nxgo-lsio_media:/media - - test_nxgo-lsio_backup:/backup - - test_nxgo-lsio_analytics:/analytics - - # Dockerfile : NxMeta - # Port : 7102 - nxmeta: - image: docker.io/ptr727/nxmeta:develop - container_name: nxmeta-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7102:7001 - volumes: - - test_nxmeta_etc:/opt/networkoptix-metavms/mediaserver/etc - - test_nxmeta_ini:/home/networkoptix-metavms/.config/nx_ini - - test_nxmeta_var:/opt/networkoptix-metavms/mediaserver/var - - test_nxmeta_media:/media - - test_nxmeta_backup:/backup - - test_nxmeta_analytics:/analytics - - # Dockerfile : NxMeta-LSIO - # Port : 7202 - nxmeta-lsio: - image: docker.io/ptr727/nxmeta-lsio:develop - container_name: nxmeta-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7202:7001 - volumes: - - test_nxmeta-lsio_config:/config - - test_nxmeta-lsio_media:/media - - test_nxmeta-lsio_backup:/backup - - test_nxmeta-lsio_analytics:/analytics - - # Dockerfile : NxWitness - # Port : 7103 - nxwitness: - image: docker.io/ptr727/nxwitness:develop - container_name: nxwitness-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7103:7001 - volumes: - - test_nxwitness_etc:/opt/networkoptix/mediaserver/etc - - test_nxwitness_ini:/home/networkoptix/.config/nx_ini - - test_nxwitness_var:/opt/networkoptix/mediaserver/var - - test_nxwitness_media:/media - - test_nxwitness_backup:/backup - - test_nxwitness_analytics:/analytics - - # Dockerfile : NxWitness-LSIO - # Port : 7203 - nxwitness-lsio: - image: docker.io/ptr727/nxwitness-lsio:develop - container_name: nxwitness-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7203:7001 - volumes: - - test_nxwitness-lsio_config:/config - - test_nxwitness-lsio_media:/media - - test_nxwitness-lsio_backup:/backup - - test_nxwitness-lsio_analytics:/analytics - - # Dockerfile : DWSpectrum - # Port : 7104 - dwspectrum: - image: docker.io/ptr727/dwspectrum:develop - container_name: dwspectrum-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7104:7001 - volumes: - - test_dwspectrum_etc:/opt/digitalwatchdog/mediaserver/etc - - test_dwspectrum_ini:/home/digitalwatchdog/.config/nx_ini - - test_dwspectrum_var:/opt/digitalwatchdog/mediaserver/var - - test_dwspectrum_media:/media - - test_dwspectrum_backup:/backup - - test_dwspectrum_analytics:/analytics - - # Dockerfile : DWSpectrum-LSIO - # Port : 7204 - dwspectrum-lsio: - image: docker.io/ptr727/dwspectrum-lsio:develop - container_name: dwspectrum-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7204:7001 - volumes: - - test_dwspectrum-lsio_config:/config - - test_dwspectrum-lsio_media:/media - - test_dwspectrum-lsio_backup:/backup - - test_dwspectrum-lsio_analytics:/analytics - - # Dockerfile : WisenetWAVE - # Port : 7105 - wisenetwave: - image: docker.io/ptr727/wisenetwave:develop - container_name: wisenetwave-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7105:7001 - volumes: - - test_wisenetwave_etc:/opt/hanwha/mediaserver/etc - - test_wisenetwave_ini:/home/hanwha/.config/nx_ini - - test_wisenetwave_var:/opt/hanwha/mediaserver/var - - test_wisenetwave_media:/media - - test_wisenetwave_backup:/backup - - test_wisenetwave_analytics:/analytics - - # Dockerfile : WisenetWAVE-LSIO - # Port : 7205 - wisenetwave-lsio: - image: docker.io/ptr727/wisenetwave-lsio:develop - container_name: wisenetwave-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7205:7001 - volumes: - - test_wisenetwave-lsio_config:/config - - test_wisenetwave-lsio_media:/media - - test_wisenetwave-lsio_backup:/backup - - test_wisenetwave-lsio_analytics:/analytics - - +# Compose file created by CreateMatrix, do not modify by hand + +volumes: + + # Dockerfile : NxGo + test_nxgo_etc: + test_nxgo_ini: + test_nxgo_var: + test_nxgo_media: + test_nxgo_backup: + test_nxgo_analytics: + + # Dockerfile : NxGo-LSIO + test_nxgo-lsio_config: + test_nxgo-lsio_media: + test_nxgo-lsio_backup: + test_nxgo-lsio_analytics: + + # Dockerfile : NxMeta + test_nxmeta_etc: + test_nxmeta_ini: + test_nxmeta_var: + test_nxmeta_media: + test_nxmeta_backup: + test_nxmeta_analytics: + + # Dockerfile : NxMeta-LSIO + test_nxmeta-lsio_config: + test_nxmeta-lsio_media: + test_nxmeta-lsio_backup: + test_nxmeta-lsio_analytics: + + # Dockerfile : NxWitness + test_nxwitness_etc: + test_nxwitness_ini: + test_nxwitness_var: + test_nxwitness_media: + test_nxwitness_backup: + test_nxwitness_analytics: + + # Dockerfile : NxWitness-LSIO + test_nxwitness-lsio_config: + test_nxwitness-lsio_media: + test_nxwitness-lsio_backup: + test_nxwitness-lsio_analytics: + + # Dockerfile : DWSpectrum + test_dwspectrum_etc: + test_dwspectrum_ini: + test_dwspectrum_var: + test_dwspectrum_media: + test_dwspectrum_backup: + test_dwspectrum_analytics: + + # Dockerfile : DWSpectrum-LSIO + test_dwspectrum-lsio_config: + test_dwspectrum-lsio_media: + test_dwspectrum-lsio_backup: + test_dwspectrum-lsio_analytics: + + # Dockerfile : WisenetWAVE + test_wisenetwave_etc: + test_wisenetwave_ini: + test_wisenetwave_var: + test_wisenetwave_media: + test_wisenetwave_backup: + test_wisenetwave_analytics: + + # Dockerfile : WisenetWAVE-LSIO + test_wisenetwave-lsio_config: + test_wisenetwave-lsio_media: + test_wisenetwave-lsio_backup: + test_wisenetwave-lsio_analytics: + + +services: + + # Dockerfile : NxGo + # Port : 7101 + nxgo: + image: docker.io/ptr727/nxgo:develop + container_name: nxgo-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7101:7001 + volumes: + - test_nxgo_etc:/opt/networkoptix/mediaserver/etc + - test_nxgo_ini:/home/networkoptix/.config/nx_ini + - test_nxgo_var:/opt/networkoptix/mediaserver/var + - test_nxgo_media:/media + - test_nxgo_backup:/backup + - test_nxgo_analytics:/analytics + + # Dockerfile : NxGo-LSIO + # Port : 7201 + nxgo-lsio: + image: docker.io/ptr727/nxgo-lsio:develop + container_name: nxgo-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7201:7001 + volumes: + - test_nxgo-lsio_config:/config + - test_nxgo-lsio_media:/media + - test_nxgo-lsio_backup:/backup + - test_nxgo-lsio_analytics:/analytics + + # Dockerfile : NxMeta + # Port : 7102 + nxmeta: + image: docker.io/ptr727/nxmeta:develop + container_name: nxmeta-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7102:7001 + volumes: + - test_nxmeta_etc:/opt/networkoptix-metavms/mediaserver/etc + - test_nxmeta_ini:/home/networkoptix-metavms/.config/nx_ini + - test_nxmeta_var:/opt/networkoptix-metavms/mediaserver/var + - test_nxmeta_media:/media + - test_nxmeta_backup:/backup + - test_nxmeta_analytics:/analytics + + # Dockerfile : NxMeta-LSIO + # Port : 7202 + nxmeta-lsio: + image: docker.io/ptr727/nxmeta-lsio:develop + container_name: nxmeta-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7202:7001 + volumes: + - test_nxmeta-lsio_config:/config + - test_nxmeta-lsio_media:/media + - test_nxmeta-lsio_backup:/backup + - test_nxmeta-lsio_analytics:/analytics + + # Dockerfile : NxWitness + # Port : 7103 + nxwitness: + image: docker.io/ptr727/nxwitness:develop + container_name: nxwitness-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7103:7001 + volumes: + - test_nxwitness_etc:/opt/networkoptix/mediaserver/etc + - test_nxwitness_ini:/home/networkoptix/.config/nx_ini + - test_nxwitness_var:/opt/networkoptix/mediaserver/var + - test_nxwitness_media:/media + - test_nxwitness_backup:/backup + - test_nxwitness_analytics:/analytics + + # Dockerfile : NxWitness-LSIO + # Port : 7203 + nxwitness-lsio: + image: docker.io/ptr727/nxwitness-lsio:develop + container_name: nxwitness-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7203:7001 + volumes: + - test_nxwitness-lsio_config:/config + - test_nxwitness-lsio_media:/media + - test_nxwitness-lsio_backup:/backup + - test_nxwitness-lsio_analytics:/analytics + + # Dockerfile : DWSpectrum + # Port : 7104 + dwspectrum: + image: docker.io/ptr727/dwspectrum:develop + container_name: dwspectrum-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7104:7001 + volumes: + - test_dwspectrum_etc:/opt/digitalwatchdog/mediaserver/etc + - test_dwspectrum_ini:/home/digitalwatchdog/.config/nx_ini + - test_dwspectrum_var:/opt/digitalwatchdog/mediaserver/var + - test_dwspectrum_media:/media + - test_dwspectrum_backup:/backup + - test_dwspectrum_analytics:/analytics + + # Dockerfile : DWSpectrum-LSIO + # Port : 7204 + dwspectrum-lsio: + image: docker.io/ptr727/dwspectrum-lsio:develop + container_name: dwspectrum-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7204:7001 + volumes: + - test_dwspectrum-lsio_config:/config + - test_dwspectrum-lsio_media:/media + - test_dwspectrum-lsio_backup:/backup + - test_dwspectrum-lsio_analytics:/analytics + + # Dockerfile : WisenetWAVE + # Port : 7105 + wisenetwave: + image: docker.io/ptr727/wisenetwave:develop + container_name: wisenetwave-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7105:7001 + volumes: + - test_wisenetwave_etc:/opt/hanwha/mediaserver/etc + - test_wisenetwave_ini:/home/hanwha/.config/nx_ini + - test_wisenetwave_var:/opt/hanwha/mediaserver/var + - test_wisenetwave_media:/media + - test_wisenetwave_backup:/backup + - test_wisenetwave_analytics:/analytics + + # Dockerfile : WisenetWAVE-LSIO + # Port : 7205 + wisenetwave-lsio: + image: docker.io/ptr727/wisenetwave-lsio:develop + container_name: wisenetwave-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7205:7001 + volumes: + - test_wisenetwave-lsio_config:/config + - test_wisenetwave-lsio_media:/media + - test_wisenetwave-lsio_backup:/backup + - test_wisenetwave-lsio_analytics:/analytics diff --git a/Make/Test-latest.yml b/Make/Test-latest.yml index 6c771495..e1db1d54 100644 --- a/Make/Test-latest.yml +++ b/Make/Test-latest.yml @@ -1,258 +1,256 @@ -# Compose file created by CreateMatrix, do not modify by hand - -volumes: - - # Dockerfile : NxGo - test_nxgo_etc: - test_nxgo_ini: - test_nxgo_var: - test_nxgo_media: - test_nxgo_backup: - test_nxgo_analytics: - - # Dockerfile : NxGo-LSIO - test_nxgo-lsio_config: - test_nxgo-lsio_media: - test_nxgo-lsio_backup: - test_nxgo-lsio_analytics: - - # Dockerfile : NxMeta - test_nxmeta_etc: - test_nxmeta_ini: - test_nxmeta_var: - test_nxmeta_media: - test_nxmeta_backup: - test_nxmeta_analytics: - - # Dockerfile : NxMeta-LSIO - test_nxmeta-lsio_config: - test_nxmeta-lsio_media: - test_nxmeta-lsio_backup: - test_nxmeta-lsio_analytics: - - # Dockerfile : NxWitness - test_nxwitness_etc: - test_nxwitness_ini: - test_nxwitness_var: - test_nxwitness_media: - test_nxwitness_backup: - test_nxwitness_analytics: - - # Dockerfile : NxWitness-LSIO - test_nxwitness-lsio_config: - test_nxwitness-lsio_media: - test_nxwitness-lsio_backup: - test_nxwitness-lsio_analytics: - - # Dockerfile : DWSpectrum - test_dwspectrum_etc: - test_dwspectrum_ini: - test_dwspectrum_var: - test_dwspectrum_media: - test_dwspectrum_backup: - test_dwspectrum_analytics: - - # Dockerfile : DWSpectrum-LSIO - test_dwspectrum-lsio_config: - test_dwspectrum-lsio_media: - test_dwspectrum-lsio_backup: - test_dwspectrum-lsio_analytics: - - # Dockerfile : WisenetWAVE - test_wisenetwave_etc: - test_wisenetwave_ini: - test_wisenetwave_var: - test_wisenetwave_media: - test_wisenetwave_backup: - test_wisenetwave_analytics: - - # Dockerfile : WisenetWAVE-LSIO - test_wisenetwave-lsio_config: - test_wisenetwave-lsio_media: - test_wisenetwave-lsio_backup: - test_wisenetwave-lsio_analytics: - - -services: - - # Dockerfile : NxGo - # Port : 7101 - nxgo: - image: docker.io/ptr727/nxgo:latest - container_name: nxgo-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7101:7001 - volumes: - - test_nxgo_etc:/opt/networkoptix/mediaserver/etc - - test_nxgo_ini:/home/networkoptix/.config/nx_ini - - test_nxgo_var:/opt/networkoptix/mediaserver/var - - test_nxgo_media:/media - - test_nxgo_backup:/backup - - test_nxgo_analytics:/analytics - - # Dockerfile : NxGo-LSIO - # Port : 7201 - nxgo-lsio: - image: docker.io/ptr727/nxgo-lsio:latest - container_name: nxgo-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7201:7001 - volumes: - - test_nxgo-lsio_config:/config - - test_nxgo-lsio_media:/media - - test_nxgo-lsio_backup:/backup - - test_nxgo-lsio_analytics:/analytics - - # Dockerfile : NxMeta - # Port : 7102 - nxmeta: - image: docker.io/ptr727/nxmeta:latest - container_name: nxmeta-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7102:7001 - volumes: - - test_nxmeta_etc:/opt/networkoptix-metavms/mediaserver/etc - - test_nxmeta_ini:/home/networkoptix-metavms/.config/nx_ini - - test_nxmeta_var:/opt/networkoptix-metavms/mediaserver/var - - test_nxmeta_media:/media - - test_nxmeta_backup:/backup - - test_nxmeta_analytics:/analytics - - # Dockerfile : NxMeta-LSIO - # Port : 7202 - nxmeta-lsio: - image: docker.io/ptr727/nxmeta-lsio:latest - container_name: nxmeta-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7202:7001 - volumes: - - test_nxmeta-lsio_config:/config - - test_nxmeta-lsio_media:/media - - test_nxmeta-lsio_backup:/backup - - test_nxmeta-lsio_analytics:/analytics - - # Dockerfile : NxWitness - # Port : 7103 - nxwitness: - image: docker.io/ptr727/nxwitness:latest - container_name: nxwitness-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7103:7001 - volumes: - - test_nxwitness_etc:/opt/networkoptix/mediaserver/etc - - test_nxwitness_ini:/home/networkoptix/.config/nx_ini - - test_nxwitness_var:/opt/networkoptix/mediaserver/var - - test_nxwitness_media:/media - - test_nxwitness_backup:/backup - - test_nxwitness_analytics:/analytics - - # Dockerfile : NxWitness-LSIO - # Port : 7203 - nxwitness-lsio: - image: docker.io/ptr727/nxwitness-lsio:latest - container_name: nxwitness-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7203:7001 - volumes: - - test_nxwitness-lsio_config:/config - - test_nxwitness-lsio_media:/media - - test_nxwitness-lsio_backup:/backup - - test_nxwitness-lsio_analytics:/analytics - - # Dockerfile : DWSpectrum - # Port : 7104 - dwspectrum: - image: docker.io/ptr727/dwspectrum:latest - container_name: dwspectrum-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7104:7001 - volumes: - - test_dwspectrum_etc:/opt/digitalwatchdog/mediaserver/etc - - test_dwspectrum_ini:/home/digitalwatchdog/.config/nx_ini - - test_dwspectrum_var:/opt/digitalwatchdog/mediaserver/var - - test_dwspectrum_media:/media - - test_dwspectrum_backup:/backup - - test_dwspectrum_analytics:/analytics - - # Dockerfile : DWSpectrum-LSIO - # Port : 7204 - dwspectrum-lsio: - image: docker.io/ptr727/dwspectrum-lsio:latest - container_name: dwspectrum-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7204:7001 - volumes: - - test_dwspectrum-lsio_config:/config - - test_dwspectrum-lsio_media:/media - - test_dwspectrum-lsio_backup:/backup - - test_dwspectrum-lsio_analytics:/analytics - - # Dockerfile : WisenetWAVE - # Port : 7105 - wisenetwave: - image: docker.io/ptr727/wisenetwave:latest - container_name: wisenetwave-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7105:7001 - volumes: - - test_wisenetwave_etc:/opt/hanwha/mediaserver/etc - - test_wisenetwave_ini:/home/hanwha/.config/nx_ini - - test_wisenetwave_var:/opt/hanwha/mediaserver/var - - test_wisenetwave_media:/media - - test_wisenetwave_backup:/backup - - test_wisenetwave_analytics:/analytics - - # Dockerfile : WisenetWAVE-LSIO - # Port : 7205 - wisenetwave-lsio: - image: docker.io/ptr727/wisenetwave-lsio:latest - container_name: wisenetwave-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7205:7001 - volumes: - - test_wisenetwave-lsio_config:/config - - test_wisenetwave-lsio_media:/media - - test_wisenetwave-lsio_backup:/backup - - test_wisenetwave-lsio_analytics:/analytics - - +# Compose file created by CreateMatrix, do not modify by hand + +volumes: + + # Dockerfile : NxGo + test_nxgo_etc: + test_nxgo_ini: + test_nxgo_var: + test_nxgo_media: + test_nxgo_backup: + test_nxgo_analytics: + + # Dockerfile : NxGo-LSIO + test_nxgo-lsio_config: + test_nxgo-lsio_media: + test_nxgo-lsio_backup: + test_nxgo-lsio_analytics: + + # Dockerfile : NxMeta + test_nxmeta_etc: + test_nxmeta_ini: + test_nxmeta_var: + test_nxmeta_media: + test_nxmeta_backup: + test_nxmeta_analytics: + + # Dockerfile : NxMeta-LSIO + test_nxmeta-lsio_config: + test_nxmeta-lsio_media: + test_nxmeta-lsio_backup: + test_nxmeta-lsio_analytics: + + # Dockerfile : NxWitness + test_nxwitness_etc: + test_nxwitness_ini: + test_nxwitness_var: + test_nxwitness_media: + test_nxwitness_backup: + test_nxwitness_analytics: + + # Dockerfile : NxWitness-LSIO + test_nxwitness-lsio_config: + test_nxwitness-lsio_media: + test_nxwitness-lsio_backup: + test_nxwitness-lsio_analytics: + + # Dockerfile : DWSpectrum + test_dwspectrum_etc: + test_dwspectrum_ini: + test_dwspectrum_var: + test_dwspectrum_media: + test_dwspectrum_backup: + test_dwspectrum_analytics: + + # Dockerfile : DWSpectrum-LSIO + test_dwspectrum-lsio_config: + test_dwspectrum-lsio_media: + test_dwspectrum-lsio_backup: + test_dwspectrum-lsio_analytics: + + # Dockerfile : WisenetWAVE + test_wisenetwave_etc: + test_wisenetwave_ini: + test_wisenetwave_var: + test_wisenetwave_media: + test_wisenetwave_backup: + test_wisenetwave_analytics: + + # Dockerfile : WisenetWAVE-LSIO + test_wisenetwave-lsio_config: + test_wisenetwave-lsio_media: + test_wisenetwave-lsio_backup: + test_wisenetwave-lsio_analytics: + + +services: + + # Dockerfile : NxGo + # Port : 7101 + nxgo: + image: docker.io/ptr727/nxgo:latest + container_name: nxgo-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7101:7001 + volumes: + - test_nxgo_etc:/opt/networkoptix/mediaserver/etc + - test_nxgo_ini:/home/networkoptix/.config/nx_ini + - test_nxgo_var:/opt/networkoptix/mediaserver/var + - test_nxgo_media:/media + - test_nxgo_backup:/backup + - test_nxgo_analytics:/analytics + + # Dockerfile : NxGo-LSIO + # Port : 7201 + nxgo-lsio: + image: docker.io/ptr727/nxgo-lsio:latest + container_name: nxgo-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7201:7001 + volumes: + - test_nxgo-lsio_config:/config + - test_nxgo-lsio_media:/media + - test_nxgo-lsio_backup:/backup + - test_nxgo-lsio_analytics:/analytics + + # Dockerfile : NxMeta + # Port : 7102 + nxmeta: + image: docker.io/ptr727/nxmeta:latest + container_name: nxmeta-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7102:7001 + volumes: + - test_nxmeta_etc:/opt/networkoptix-metavms/mediaserver/etc + - test_nxmeta_ini:/home/networkoptix-metavms/.config/nx_ini + - test_nxmeta_var:/opt/networkoptix-metavms/mediaserver/var + - test_nxmeta_media:/media + - test_nxmeta_backup:/backup + - test_nxmeta_analytics:/analytics + + # Dockerfile : NxMeta-LSIO + # Port : 7202 + nxmeta-lsio: + image: docker.io/ptr727/nxmeta-lsio:latest + container_name: nxmeta-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7202:7001 + volumes: + - test_nxmeta-lsio_config:/config + - test_nxmeta-lsio_media:/media + - test_nxmeta-lsio_backup:/backup + - test_nxmeta-lsio_analytics:/analytics + + # Dockerfile : NxWitness + # Port : 7103 + nxwitness: + image: docker.io/ptr727/nxwitness:latest + container_name: nxwitness-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7103:7001 + volumes: + - test_nxwitness_etc:/opt/networkoptix/mediaserver/etc + - test_nxwitness_ini:/home/networkoptix/.config/nx_ini + - test_nxwitness_var:/opt/networkoptix/mediaserver/var + - test_nxwitness_media:/media + - test_nxwitness_backup:/backup + - test_nxwitness_analytics:/analytics + + # Dockerfile : NxWitness-LSIO + # Port : 7203 + nxwitness-lsio: + image: docker.io/ptr727/nxwitness-lsio:latest + container_name: nxwitness-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7203:7001 + volumes: + - test_nxwitness-lsio_config:/config + - test_nxwitness-lsio_media:/media + - test_nxwitness-lsio_backup:/backup + - test_nxwitness-lsio_analytics:/analytics + + # Dockerfile : DWSpectrum + # Port : 7104 + dwspectrum: + image: docker.io/ptr727/dwspectrum:latest + container_name: dwspectrum-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7104:7001 + volumes: + - test_dwspectrum_etc:/opt/digitalwatchdog/mediaserver/etc + - test_dwspectrum_ini:/home/digitalwatchdog/.config/nx_ini + - test_dwspectrum_var:/opt/digitalwatchdog/mediaserver/var + - test_dwspectrum_media:/media + - test_dwspectrum_backup:/backup + - test_dwspectrum_analytics:/analytics + + # Dockerfile : DWSpectrum-LSIO + # Port : 7204 + dwspectrum-lsio: + image: docker.io/ptr727/dwspectrum-lsio:latest + container_name: dwspectrum-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7204:7001 + volumes: + - test_dwspectrum-lsio_config:/config + - test_dwspectrum-lsio_media:/media + - test_dwspectrum-lsio_backup:/backup + - test_dwspectrum-lsio_analytics:/analytics + + # Dockerfile : WisenetWAVE + # Port : 7105 + wisenetwave: + image: docker.io/ptr727/wisenetwave:latest + container_name: wisenetwave-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7105:7001 + volumes: + - test_wisenetwave_etc:/opt/hanwha/mediaserver/etc + - test_wisenetwave_ini:/home/hanwha/.config/nx_ini + - test_wisenetwave_var:/opt/hanwha/mediaserver/var + - test_wisenetwave_media:/media + - test_wisenetwave_backup:/backup + - test_wisenetwave_analytics:/analytics + + # Dockerfile : WisenetWAVE-LSIO + # Port : 7205 + wisenetwave-lsio: + image: docker.io/ptr727/wisenetwave-lsio:latest + container_name: wisenetwave-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7205:7001 + volumes: + - test_wisenetwave-lsio_config:/config + - test_wisenetwave-lsio_media:/media + - test_wisenetwave-lsio_backup:/backup + - test_wisenetwave-lsio_analytics:/analytics diff --git a/Make/Test.yml b/Make/Test.yml index 42bc581f..0e382a36 100644 --- a/Make/Test.yml +++ b/Make/Test.yml @@ -1,258 +1,256 @@ -# Compose file created by CreateMatrix, do not modify by hand - -volumes: - - # Dockerfile : NxGo - test_nxgo_etc: - test_nxgo_ini: - test_nxgo_var: - test_nxgo_media: - test_nxgo_backup: - test_nxgo_analytics: - - # Dockerfile : NxGo-LSIO - test_nxgo-lsio_config: - test_nxgo-lsio_media: - test_nxgo-lsio_backup: - test_nxgo-lsio_analytics: - - # Dockerfile : NxMeta - test_nxmeta_etc: - test_nxmeta_ini: - test_nxmeta_var: - test_nxmeta_media: - test_nxmeta_backup: - test_nxmeta_analytics: - - # Dockerfile : NxMeta-LSIO - test_nxmeta-lsio_config: - test_nxmeta-lsio_media: - test_nxmeta-lsio_backup: - test_nxmeta-lsio_analytics: - - # Dockerfile : NxWitness - test_nxwitness_etc: - test_nxwitness_ini: - test_nxwitness_var: - test_nxwitness_media: - test_nxwitness_backup: - test_nxwitness_analytics: - - # Dockerfile : NxWitness-LSIO - test_nxwitness-lsio_config: - test_nxwitness-lsio_media: - test_nxwitness-lsio_backup: - test_nxwitness-lsio_analytics: - - # Dockerfile : DWSpectrum - test_dwspectrum_etc: - test_dwspectrum_ini: - test_dwspectrum_var: - test_dwspectrum_media: - test_dwspectrum_backup: - test_dwspectrum_analytics: - - # Dockerfile : DWSpectrum-LSIO - test_dwspectrum-lsio_config: - test_dwspectrum-lsio_media: - test_dwspectrum-lsio_backup: - test_dwspectrum-lsio_analytics: - - # Dockerfile : WisenetWAVE - test_wisenetwave_etc: - test_wisenetwave_ini: - test_wisenetwave_var: - test_wisenetwave_media: - test_wisenetwave_backup: - test_wisenetwave_analytics: - - # Dockerfile : WisenetWAVE-LSIO - test_wisenetwave-lsio_config: - test_wisenetwave-lsio_media: - test_wisenetwave-lsio_backup: - test_wisenetwave-lsio_analytics: - - -services: - - # Dockerfile : NxGo - # Port : 7101 - nxgo: - image: test_nxgo - container_name: nxgo-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7101:7001 - volumes: - - test_nxgo_etc:/opt/networkoptix/mediaserver/etc - - test_nxgo_ini:/home/networkoptix/.config/nx_ini - - test_nxgo_var:/opt/networkoptix/mediaserver/var - - test_nxgo_media:/media - - test_nxgo_backup:/backup - - test_nxgo_analytics:/analytics - - # Dockerfile : NxGo-LSIO - # Port : 7201 - nxgo-lsio: - image: test_nxgo-lsio - container_name: nxgo-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7201:7001 - volumes: - - test_nxgo-lsio_config:/config - - test_nxgo-lsio_media:/media - - test_nxgo-lsio_backup:/backup - - test_nxgo-lsio_analytics:/analytics - - # Dockerfile : NxMeta - # Port : 7102 - nxmeta: - image: test_nxmeta - container_name: nxmeta-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7102:7001 - volumes: - - test_nxmeta_etc:/opt/networkoptix-metavms/mediaserver/etc - - test_nxmeta_ini:/home/networkoptix-metavms/.config/nx_ini - - test_nxmeta_var:/opt/networkoptix-metavms/mediaserver/var - - test_nxmeta_media:/media - - test_nxmeta_backup:/backup - - test_nxmeta_analytics:/analytics - - # Dockerfile : NxMeta-LSIO - # Port : 7202 - nxmeta-lsio: - image: test_nxmeta-lsio - container_name: nxmeta-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7202:7001 - volumes: - - test_nxmeta-lsio_config:/config - - test_nxmeta-lsio_media:/media - - test_nxmeta-lsio_backup:/backup - - test_nxmeta-lsio_analytics:/analytics - - # Dockerfile : NxWitness - # Port : 7103 - nxwitness: - image: test_nxwitness - container_name: nxwitness-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7103:7001 - volumes: - - test_nxwitness_etc:/opt/networkoptix/mediaserver/etc - - test_nxwitness_ini:/home/networkoptix/.config/nx_ini - - test_nxwitness_var:/opt/networkoptix/mediaserver/var - - test_nxwitness_media:/media - - test_nxwitness_backup:/backup - - test_nxwitness_analytics:/analytics - - # Dockerfile : NxWitness-LSIO - # Port : 7203 - nxwitness-lsio: - image: test_nxwitness-lsio - container_name: nxwitness-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7203:7001 - volumes: - - test_nxwitness-lsio_config:/config - - test_nxwitness-lsio_media:/media - - test_nxwitness-lsio_backup:/backup - - test_nxwitness-lsio_analytics:/analytics - - # Dockerfile : DWSpectrum - # Port : 7104 - dwspectrum: - image: test_dwspectrum - container_name: dwspectrum-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7104:7001 - volumes: - - test_dwspectrum_etc:/opt/digitalwatchdog/mediaserver/etc - - test_dwspectrum_ini:/home/digitalwatchdog/.config/nx_ini - - test_dwspectrum_var:/opt/digitalwatchdog/mediaserver/var - - test_dwspectrum_media:/media - - test_dwspectrum_backup:/backup - - test_dwspectrum_analytics:/analytics - - # Dockerfile : DWSpectrum-LSIO - # Port : 7204 - dwspectrum-lsio: - image: test_dwspectrum-lsio - container_name: dwspectrum-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7204:7001 - volumes: - - test_dwspectrum-lsio_config:/config - - test_dwspectrum-lsio_media:/media - - test_dwspectrum-lsio_backup:/backup - - test_dwspectrum-lsio_analytics:/analytics - - # Dockerfile : WisenetWAVE - # Port : 7105 - wisenetwave: - image: test_wisenetwave - container_name: wisenetwave-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7105:7001 - volumes: - - test_wisenetwave_etc:/opt/hanwha/mediaserver/etc - - test_wisenetwave_ini:/home/hanwha/.config/nx_ini - - test_wisenetwave_var:/opt/hanwha/mediaserver/var - - test_wisenetwave_media:/media - - test_wisenetwave_backup:/backup - - test_wisenetwave_analytics:/analytics - - # Dockerfile : WisenetWAVE-LSIO - # Port : 7205 - wisenetwave-lsio: - image: test_wisenetwave-lsio - container_name: wisenetwave-lsio-container - restart: unless-stopped - environment: - - TZ=Americas/Los_Angeles - network_mode: bridge - ports: - - 7205:7001 - volumes: - - test_wisenetwave-lsio_config:/config - - test_wisenetwave-lsio_media:/media - - test_wisenetwave-lsio_backup:/backup - - test_wisenetwave-lsio_analytics:/analytics - - +# Compose file created by CreateMatrix, do not modify by hand + +volumes: + + # Dockerfile : NxGo + test_nxgo_etc: + test_nxgo_ini: + test_nxgo_var: + test_nxgo_media: + test_nxgo_backup: + test_nxgo_analytics: + + # Dockerfile : NxGo-LSIO + test_nxgo-lsio_config: + test_nxgo-lsio_media: + test_nxgo-lsio_backup: + test_nxgo-lsio_analytics: + + # Dockerfile : NxMeta + test_nxmeta_etc: + test_nxmeta_ini: + test_nxmeta_var: + test_nxmeta_media: + test_nxmeta_backup: + test_nxmeta_analytics: + + # Dockerfile : NxMeta-LSIO + test_nxmeta-lsio_config: + test_nxmeta-lsio_media: + test_nxmeta-lsio_backup: + test_nxmeta-lsio_analytics: + + # Dockerfile : NxWitness + test_nxwitness_etc: + test_nxwitness_ini: + test_nxwitness_var: + test_nxwitness_media: + test_nxwitness_backup: + test_nxwitness_analytics: + + # Dockerfile : NxWitness-LSIO + test_nxwitness-lsio_config: + test_nxwitness-lsio_media: + test_nxwitness-lsio_backup: + test_nxwitness-lsio_analytics: + + # Dockerfile : DWSpectrum + test_dwspectrum_etc: + test_dwspectrum_ini: + test_dwspectrum_var: + test_dwspectrum_media: + test_dwspectrum_backup: + test_dwspectrum_analytics: + + # Dockerfile : DWSpectrum-LSIO + test_dwspectrum-lsio_config: + test_dwspectrum-lsio_media: + test_dwspectrum-lsio_backup: + test_dwspectrum-lsio_analytics: + + # Dockerfile : WisenetWAVE + test_wisenetwave_etc: + test_wisenetwave_ini: + test_wisenetwave_var: + test_wisenetwave_media: + test_wisenetwave_backup: + test_wisenetwave_analytics: + + # Dockerfile : WisenetWAVE-LSIO + test_wisenetwave-lsio_config: + test_wisenetwave-lsio_media: + test_wisenetwave-lsio_backup: + test_wisenetwave-lsio_analytics: + + +services: + + # Dockerfile : NxGo + # Port : 7101 + nxgo: + image: test_nxgo + container_name: nxgo-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7101:7001 + volumes: + - test_nxgo_etc:/opt/networkoptix/mediaserver/etc + - test_nxgo_ini:/home/networkoptix/.config/nx_ini + - test_nxgo_var:/opt/networkoptix/mediaserver/var + - test_nxgo_media:/media + - test_nxgo_backup:/backup + - test_nxgo_analytics:/analytics + + # Dockerfile : NxGo-LSIO + # Port : 7201 + nxgo-lsio: + image: test_nxgo-lsio + container_name: nxgo-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7201:7001 + volumes: + - test_nxgo-lsio_config:/config + - test_nxgo-lsio_media:/media + - test_nxgo-lsio_backup:/backup + - test_nxgo-lsio_analytics:/analytics + + # Dockerfile : NxMeta + # Port : 7102 + nxmeta: + image: test_nxmeta + container_name: nxmeta-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7102:7001 + volumes: + - test_nxmeta_etc:/opt/networkoptix-metavms/mediaserver/etc + - test_nxmeta_ini:/home/networkoptix-metavms/.config/nx_ini + - test_nxmeta_var:/opt/networkoptix-metavms/mediaserver/var + - test_nxmeta_media:/media + - test_nxmeta_backup:/backup + - test_nxmeta_analytics:/analytics + + # Dockerfile : NxMeta-LSIO + # Port : 7202 + nxmeta-lsio: + image: test_nxmeta-lsio + container_name: nxmeta-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7202:7001 + volumes: + - test_nxmeta-lsio_config:/config + - test_nxmeta-lsio_media:/media + - test_nxmeta-lsio_backup:/backup + - test_nxmeta-lsio_analytics:/analytics + + # Dockerfile : NxWitness + # Port : 7103 + nxwitness: + image: test_nxwitness + container_name: nxwitness-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7103:7001 + volumes: + - test_nxwitness_etc:/opt/networkoptix/mediaserver/etc + - test_nxwitness_ini:/home/networkoptix/.config/nx_ini + - test_nxwitness_var:/opt/networkoptix/mediaserver/var + - test_nxwitness_media:/media + - test_nxwitness_backup:/backup + - test_nxwitness_analytics:/analytics + + # Dockerfile : NxWitness-LSIO + # Port : 7203 + nxwitness-lsio: + image: test_nxwitness-lsio + container_name: nxwitness-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7203:7001 + volumes: + - test_nxwitness-lsio_config:/config + - test_nxwitness-lsio_media:/media + - test_nxwitness-lsio_backup:/backup + - test_nxwitness-lsio_analytics:/analytics + + # Dockerfile : DWSpectrum + # Port : 7104 + dwspectrum: + image: test_dwspectrum + container_name: dwspectrum-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7104:7001 + volumes: + - test_dwspectrum_etc:/opt/digitalwatchdog/mediaserver/etc + - test_dwspectrum_ini:/home/digitalwatchdog/.config/nx_ini + - test_dwspectrum_var:/opt/digitalwatchdog/mediaserver/var + - test_dwspectrum_media:/media + - test_dwspectrum_backup:/backup + - test_dwspectrum_analytics:/analytics + + # Dockerfile : DWSpectrum-LSIO + # Port : 7204 + dwspectrum-lsio: + image: test_dwspectrum-lsio + container_name: dwspectrum-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7204:7001 + volumes: + - test_dwspectrum-lsio_config:/config + - test_dwspectrum-lsio_media:/media + - test_dwspectrum-lsio_backup:/backup + - test_dwspectrum-lsio_analytics:/analytics + + # Dockerfile : WisenetWAVE + # Port : 7105 + wisenetwave: + image: test_wisenetwave + container_name: wisenetwave-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7105:7001 + volumes: + - test_wisenetwave_etc:/opt/hanwha/mediaserver/etc + - test_wisenetwave_ini:/home/hanwha/.config/nx_ini + - test_wisenetwave_var:/opt/hanwha/mediaserver/var + - test_wisenetwave_media:/media + - test_wisenetwave_backup:/backup + - test_wisenetwave_analytics:/analytics + + # Dockerfile : WisenetWAVE-LSIO + # Port : 7205 + wisenetwave-lsio: + image: test_wisenetwave-lsio + container_name: wisenetwave-lsio-container + restart: unless-stopped + environment: + - TZ=Americas/Los_Angeles + network_mode: bridge + ports: + - 7205:7001 + volumes: + - test_wisenetwave-lsio_config:/config + - test_wisenetwave-lsio_media:/media + - test_wisenetwave-lsio_backup:/backup + - test_wisenetwave-lsio_analytics:/analytics diff --git a/Make/Version.json b/Make/Version.json index 63cff28d..b44e7a34 100644 --- a/Make/Version.json +++ b/Make/Version.json @@ -1,110 +1,110 @@ -{ - "SchemaVersion": 2, - "Products": [ - { - "Product": "NxGo", - "Versions": [ - { - "Version": "6.1.2.42921", - "UriX64": "https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", - "Labels": [ - "Stable" - ] - }, - { - "Version": "6.1.3.43301", - "UriX64": "https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", - "Labels": [ - "Latest" - ] - } - ] - }, - { - "Product": "NxMeta", - "Versions": [ - { - "Version": "6.1.2.42921", - "UriX64": "https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", - "Labels": [ - "Stable" - ] - }, - { - "Version": "6.1.3.43301", - "UriX64": "https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", - "Labels": [ - "Latest" - ] - } - ] - }, - { - "Product": "NxWitness", - "Versions": [ - { - "Version": "6.1.2.42921", - "UriX64": "https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", - "Labels": [ - "Stable" - ] - }, - { - "Version": "6.1.3.43301", - "UriX64": "https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", - "Labels": [ - "Latest" - ] - } - ] - }, - { - "Product": "DWSpectrum", - "Versions": [ - { - "Version": "6.1.1.42624", - "UriX64": "https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", - "Labels": [ - "Stable" - ] - }, - { - "Version": "6.1.3.43301", - "UriX64": "https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", - "Labels": [ - "Latest" - ] - } - ] - }, - { - "Product": "WisenetWAVE", - "Versions": [ - { - "Version": "6.1.2.42921", - "UriX64": "https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", - "Labels": [ - "Stable" - ] - }, - { - "Version": "6.1.3.43301", - "UriX64": "https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", - "Labels": [ - "Latest" - ] - } - ] - } - ] +{ + "SchemaVersion": 2, + "Products": [ + { + "Product": "NxGo", + "Versions": [ + { + "Version": "6.1.2.42921", + "UriX64": "https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", + "Labels": [ + "Stable" + ] + }, + { + "Version": "6.1.3.43301", + "UriX64": "https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/nxgo/43301/nxgo-server_update-6.1.3.43301-linux_arm64.zip", + "Labels": [ + "Latest" + ] + } + ] + }, + { + "Product": "NxMeta", + "Versions": [ + { + "Version": "6.1.2.42921", + "UriX64": "https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", + "Labels": [ + "Stable" + ] + }, + { + "Version": "6.1.3.43301", + "UriX64": "https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/metavms/43301/metavms-server_update-6.1.3.43301-linux_arm64.zip", + "Labels": [ + "Latest" + ] + } + ] + }, + { + "Product": "NxWitness", + "Versions": [ + { + "Version": "6.1.2.42921", + "UriX64": "https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", + "Labels": [ + "Stable" + ] + }, + { + "Version": "6.1.3.43301", + "UriX64": "https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/default/43301/nxwitness-server_update-6.1.3.43301-linux_arm64.zip", + "Labels": [ + "Latest" + ] + } + ] + }, + { + "Product": "DWSpectrum", + "Versions": [ + { + "Version": "6.1.1.42624", + "UriX64": "https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip", + "Labels": [ + "Stable" + ] + }, + { + "Version": "6.1.3.43301", + "UriX64": "https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/digitalwatchdog/43301/dwspectrum-server_update-6.1.3.43301-linux_arm64.zip", + "Labels": [ + "Latest" + ] + } + ] + }, + { + "Product": "WisenetWAVE", + "Versions": [ + { + "Version": "6.1.2.42921", + "UriX64": "https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", + "Labels": [ + "Stable" + ] + }, + { + "Version": "6.1.3.43301", + "UriX64": "https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/hanwha/43301/wave-server_update-6.1.3.43301-linux_arm64.zip", + "Labels": [ + "Latest" + ] + } + ] + } + ] } \ No newline at end of file diff --git a/NxWitness.code-workspace b/NxWitness.code-workspace index b9553aba..beb4d115 100644 --- a/NxWitness.code-workspace +++ b/NxWitness.code-workspace @@ -5,105 +5,6 @@ } ], "settings": { - "cSpell.words": [ - "accessibilities", - "adduser", - "Allman", - "apikey", - "appdata", - "autoremove", - "btrfs", - "BUILDPLATFORM", - "buildtransitive", - "buildx", - "cloudhost", - "codegen", - "Composefile", - "connrefused", - "contentfiles", - "csdevkit", - "davidanson", - "digitalwatchdog", - "dockerdirectory", - "dockerenv", - "Dockerfiles", - "domainname", - "dotnettools", - "dwspectrum", - "esac", - "filesystems", - "getent", - "getmatrix", - "gruntfuggly", - "Hanwha", - "hddpool", - "IPVMS", - "kinnairdclan", - "lsio", - "lsiobase", - "macvlan", - "makedirectory", - "matrixpath", - "matrixschemapath", - "mediaserver", - "metavms", - "Moby", - "nameof", - "networkoptix", - "Newtonsoft", - "noatime", - "nodev", - "noninteractive", - "NONROOT", - "NOPASSWD", - "nosuid", - "nvrpool", - "nxgo", - "NXMETA", - "nxvms", - "NXWITNESS", - "Optix", - "PGID", - "Pieter", - "pipefail", - "posixacl", - "PUID", - "relatime", - "repoint", - "repointed", - "Repot", - "resharper", - "schemamatrix", - "schemaversion", - "seccomp", - "Serilog", - "shfs", - "Signup", - "slnx", - "ssdpool", - "subvol", - "subvolid", - "tagsargs", - "TARGETARCH", - "TARGETPLATFORM", - "tmpfs", - "traefik", - "trunc", - "Unraid", - "updateversion", - "usermod", - "versionlabel", - "versionpath", - "versionschemapath", - "Viljoen", - "whitelabel", - "Wisenet", - "wisenetwave", - "WORKDIR", - "xattr", - "xunit", - "yzhang" - ], "files.trimTrailingWhitespace": true, "files.trimTrailingWhitespaceInRegexAndStrings": false, "diffEditor.ignoreTrimWhitespace": false, diff --git a/NxWitness.slnx b/NxWitness.slnx index e0b717a3..91c02d80 100644 --- a/NxWitness.slnx +++ b/NxWitness.slnx @@ -1,36 +1,40 @@ -<Solution> - <Folder Name="/GitHub Actions/"> - <File Path=".github/workflows/build-base-images-task.yml" /> - <File Path=".github/workflows/build-docker-task.yml" /> - <File Path=".github/workflows/get-version-task.yml" /> - <File Path=".github/workflows/merge-bot-pull-request.yml" /> - <File Path=".github/workflows/publish-release.yml" /> - <File Path=".github/workflows/run-codegen-pull-request-task.yml" /> - <File Path=".github/workflows/run-periodic-codegen-pull-request.yml" /> - <File Path=".github/workflows/test-pull-request.yml" /> - <File Path=".github/workflows/validate-task.yml" /> - </Folder> - <Folder Name="/Repo Config/"> - <File Path="repo-config/README.md" /> - <File Path="repo-config/configure.sh" /> - <File Path="repo-config/ruleset-develop.json" /> - <File Path="repo-config/ruleset-main.json" /> - <File Path="repo-config/settings.json" /> - </Folder> - <Folder Name="/Solution Items/"> - <File Path=".dockerignore" /> - <File Path=".editorconfig" /> - <File Path=".gitattributes" /> - <File Path=".gitignore" /> - <File Path="AGENTS.md" /> - <File Path="CODESTYLE.md" /> - <File Path="LICENSE" /> - <File Path="README.md" /> - <File Path="WORKFLOW.md" /> - <File Path="version.json" /> - </Folder> - <Project Path="CreateMatrix/CreateMatrix.csproj" /> - <Project Path="CreateMatrixTests/CreateMatrixTests.csproj"> - <BuildDependency Project="CreateMatrix/CreateMatrix.csproj" /> - </Project> -</Solution> +<Solution> + <Folder Name="/GitHub Actions/"> + <File Path=".github/workflows/build-base-images-task.yml" /> + <File Path=".github/workflows/build-docker-task.yml" /> + <File Path=".github/workflows/get-version-task.yml" /> + <File Path=".github/workflows/merge-bot-pull-request.yml" /> + <File Path=".github/workflows/publish-release.yml" /> + <File Path=".github/workflows/run-codegen-pull-request-task.yml" /> + <File Path=".github/workflows/run-periodic-codegen-pull-request.yml" /> + <File Path=".github/workflows/test-pull-request.yml" /> + <File Path=".github/workflows/validate-task.yml" /> + </Folder> + <Folder Name="/Solution Items/"> + <File Path=".dockerignore" /> + <File Path=".editorconfig" /> + <File Path=".editorconfig-checker.json" /> + <File Path=".gitattributes" /> + <File Path=".gitignore" /> + <File Path=".markdownlint-cli2.jsonc" /> + <File Path="AGENTS.md" /> + <File Path="ARCHITECTURE.md" /> + <File Path="CLAUDE.md" /> + <File Path="CODESTYLE.md" /> + <File Path="GOVERNANCE.md" /> + <File Path="HISTORY.md" /> + <File Path="LICENSE" /> + <File Path="OPERATIONS.md" /> + <File Path="README.md" /> + <File Path="WORKFLOW.md" /> + <File Path="codecov.yml" /> + <File Path="cspell.json" /> + <File Path="global.json" /> + <File Path="host-tools.json" /> + <File Path="version.json" /> + </Folder> + <Project Path="CreateMatrix/CreateMatrix.csproj" /> + <Project Path="CreateMatrixTests/CreateMatrixTests.csproj"> + <BuildDependency Project="CreateMatrix/CreateMatrix.csproj" /> + </Project> +</Solution> diff --git a/OPERATIONS.md b/OPERATIONS.md new file mode 100644 index 00000000..205b854a --- /dev/null +++ b/OPERATIONS.md @@ -0,0 +1,69 @@ +# Operations + +How this repo is run: what verifying a change requires before it is pushed, the commands that regenerate and build the images, and where the repo-specific tooling is configured. + +## Local Verification + +Verifying a change here is two things: running the gates, and then running by hand the one part of the contract the gates never reach. + +The gates are the .NET clean-compile (the `.NET Format` VS Code task, per [CODESTYLE.md](./CODESTYLE.md)), the unit tests, and the document linters. [GOVERNANCE.md "Running the Linters Locally"](./GOVERNANCE.md#running-the-linters-locally-known-working-invocations) carries the known-working invocation of each linter, and CI runs the same set in [validate-task.yml](./.github/workflows/validate-task.yml), so a local lint run buys an earlier failure rather than a different one. **Linting is not editor-only here**: `validate-task.yml` runs markdownlint, CSpell, `actionlint`, and `editorconfig-checker` alongside the Husky style checks and `dotnet test`, all inside the required check. + +**What CI structurally cannot exercise is the product image matrix.** The pull-request pipeline builds a deliberate smoke subset: NxMeta and NxMeta-LSIO, amd64 only, never pushed, and only when `Docker/**`, `Make/Matrix.json`, or `Make/Version.json` changed. Eight of the ten product images, the arm64 leg of every image, the base image push path, and a container that actually starts and serves its web UI are all unbuilt at merge time. The publish run is the first thing that builds them, and a workflow-only edit is deliberately not smoke-built at all. So a change to a Dockerfile, a build arg, a base image, or the generated matrix is verified locally by running `./Create.sh` and `./Build.sh` from inside `Make/`, which build every product image for both `linux/amd64` and `linux/arm64`, and, when the change can affect a running server, `./Test.sh` followed by `./Instructions.sh` there to reach each product's web UI. Reading a green pipeline as coverage of the full matrix is the mistake this section exists to name. + +## Runbooks + +### Regenerate the Version, Matrix, and Dockerfiles + +The primary developer entry points are the `CreateMatrix` CLI commands, invoked directly or through the scripts in `Make/`: + +```sh +dotnet run --project ./CreateMatrix/CreateMatrix.csproj -- version --versionpath=./Make/Version.json +dotnet run --project ./CreateMatrix/CreateMatrix.csproj -- matrix --versionpath=./Make/Version.json --matrixpath=./Make/Matrix.json --updateversion +dotnet run --project ./CreateMatrix/CreateMatrix.csproj -- make --versionpath=./Make/Version.json --makedirectory=./Make --dockerdirectory=./Docker +``` + +`version`, `matrix` and `make` are subcommands of the `CreateMatrix` executable rather than programs on `PATH`, so they are reached through `dotnet run` from the repository root. `make` in particular is not the system `make`. `--versionlabel` is left off so this matches what `Make/Create.sh` runs, which takes the `Latest` default; passing a different label selects different product versions and so generates different Dockerfiles. + +`Docker/` and the Compose files in `Make/` are generated output, so a change to the generator is committed together with its regenerated output. + +### Build and Run the Images Locally + +The scripts in `Make/` wrap the generator and the Docker build, and each is run from inside `Make/`: + +- `Create.sh` updates `Version.json` and `Matrix.json` and writes the Dockerfiles. +- `Build.sh` builds the base images and every product image for `linux/amd64` and `linux/arm64`, and loads the amd64 targets. Setting `PUSH_BASE_IMAGES=true` pushes the base images, which needs a Docker Hub login. +- `Test.sh` runs `Create.sh`, then `Build.sh`, then `Up.sh`. +- `Up.sh`, `Up-latest.sh`, and `Up-develop.sh` bring up the locally built (`Test.yml`), released, and develop Compose stacks. `Down.sh`, `Down-latest.sh`, and `Down-develop.sh` bring the matching stack back down. +- `Instructions.sh` prints the web UI URL of each product in the running stack. +- `Clean.sh` shuts the stack down and deletes the images. + +## Backup and Recovery + +There is no state to back up. The repository is the record and GitHub holds it, every published image is rebuilt from the pinned inputs in `Make/Matrix.json` and `Make/Version.json`, and a local Compose stack is disposable by design, since `Clean.sh` deletes it outright. Recovering a published image means re-running the publisher against the commit that carries that pin, not restoring anything. + +## Logs and Debugging + +Workflow runs are the log for anything that happened in CI or in a publish. `gh run list --branch <branch>` and `gh run view <id> --log-failed` reach them. A gate failure reproduces locally, because CI runs the same commands against the same committed configuration, so reproduce it locally before reading workflow logs. + +A failure that appears only in a built image needs a running container instead. Bring the stack up with `./Up.sh` from inside `Make/`, find the product's web UI with `./Instructions.sh` there, read `docker logs <container>`, and attach a shell with `docker exec --interactive --tty <container> /bin/bash`. The mediaserver's own logging is a product setting rather than a container one: set `logLevel=verbose` in `mediaserver.conf`, restart the server, and read `/config/var/log/log_file.log` inside the container. + +## Tool Usage + +**Tests run on the native Microsoft.Testing.Platform runner**, opted into by `global.json`. The invocation CI runs, and the one to reproduce locally, is `dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage`, which then renames each report to `coverage-<guid>.cobertura.xml` so codecov-cli's file finder matches it. Under MTP a run that discovers no tests exits 5 and reports `Zero tests ran` rather than passing silently, so read the count and not just the exit status. If `dotnet test` reports zero tests on a machine where the build succeeded, run the built test application directly, `dotnet CreateMatrixTests/bin/Debug/net10.0/CreateMatrixTests.dll`, which is the same MTP host without the `dotnet test` driver in front of it, and compare. A driver that reports the target as `net10.0` where the direct run reports `net10.0|x64` has not resolved an architecture, and its zero-test result says nothing about the tests. `--coverage-output` stays unset, because pinning one filename gives every test project in the solution the same path and the last to finish overwrites the rest. Running an MTP-based test project through the VSTest target is what fails, so the `--collect:"XPlat Code Coverage"` form is an error in this repo specifically because it opted in. + +**C# formatting has an order**: CSharpier formats, then `dotnet format style` verifies. Running them the other way round means CSharpier rewrites what `dotnet format` just verified. The `.NET Format` VS Code task chains them correctly and must be clean and warning-free at all times; [CODESTYLE.md](./CODESTYLE.md) carries the full task chain and its exact arguments. + +**Husky.Net runs the pre-commit hook.** `.husky/task-runner.json` defines the task set: `CSharpier Format` over the staged `.cs` files, then `.NET Format` running `dotnet format style --verify-no-changes`. `dotnet husky run` runs that set by hand. Matching this tooling, `.vscode/tasks.json` carries a Husky.Net Run task where the fleet template carries a Benchmark task. + +**Workflow files get an editor check and a CLI check.** The GitHub Actions extension covers schema and expression checks while editing; run the `actionlint` CLI for the deeper checks, including shellcheck over `run:` steps. This matters more here than the lint gate alone suggests, because a workflow-only change is not smoke-built. + +## Configuration Layout + +- `NxWitness.code-workspace` is this repo's workspace file. Open it in VS Code rather than the folder so its settings and recommended extensions apply. +- `cspell.json` is the single source of truth for the spell-check dictionary and ignore paths. A new project word belongs in its `words` list. +- `.vscode/tasks.json` and `.vscode/launch.json` hold the build, debug, and lint tasks, including the `.NET Format` chain and the Husky.Net Run task. +- `.husky/task-runner.json` defines the pre-commit task set. +- `.markdownlint-cli2.jsonc` configures markdownlint, and `.editorconfig-checker.json` scopes the line-ending and whitespace check. +- `global.json` opts into the Microsoft.Testing.Platform test runner. It deliberately carries no `sdk` section, so SDK resolution and roll-forward stay at their defaults. `codecov.yml` configures the report-only coverage upload. +- `version.json` is the Nerdbank.GitVersioning input. `Make/Version.json` and `Make/Matrix.json` are the product version and build matrix inputs, and [ARCHITECTURE.md](./ARCHITECTURE.md) describes how they reach the images. +- `host-tools.json` declares the host tooling a development machine needs. diff --git a/README.md b/README.md index a1afffd5..379223b4 100644 --- a/README.md +++ b/README.md @@ -1,731 +1,849 @@ -# Docker Projects for Network Optix VMS Products - -This is a project to build and publish docker images for various [Network Optix][networkoptix-link] VMS products. - -## Build and Distribution - -### Build Status - -[![Release Status][releasebuildstatus-shield]][actions-link]\ -[![Last Commit][lastcommit-shield]][github-link] - -### Release Notes - -**Version: 2.14**: - -**Summary**: - -- Fixed an LSIO permissions race where the startup `chown` could run before the `PUID`/`PGID` remap, leaving `/config` and `/media` owned by the wrong user and breaking media writes. - -See [Release History](./HISTORY.md) for complete release notes and older versions. - -## Getting Started - -**Getting started with a simple test compose file**: - -```yaml -# compose.yaml - -# Test using non persistent docker volumes -volumes: - test_nxwitness-lsio_config: - test_nxwitness-lsio_media: - test_nxwitness-lsio_backup: - test_nxwitness-lsio_analytics: - -services: - nxwitness-lsio: - # Use the image matching your product - image: docker.io/ptr727/nxwitness-lsio:stable - container_name: nxwitness-lsio-test-container - restart: unless-stopped - network_mode: bridge - ports: - # Expose the service on port 7203 - - 7203:7001 - environment: - - TZ=America/Los_Angeles - volumes: - # Map to your real storage in production - - test_nxwitness-lsio_config:/config - - test_nxwitness-lsio_media:/media - - test_nxwitness-lsio_backup:/backup - - test_nxwitness-lsio_analytics:/analytics - tmpfs: - # Keep mediaserver's Unix socket and tmp files in RAM - - /tmp:size=1g,mode=1777 -``` - -```shell -# Launch the service -docker compose up --detach - -# Open your web browser on the local machine port 7203 -echo "Nx Witness LSIO:" "https://$HOSTNAME:7203/" - -# Shut the service down -docker compose down -``` - -**Example of a service in production**: - -```yaml -networks: - - public_network: # External macvlan network - name: ${PUBLIC_NETWORK_NAME} - external: true - local_network: # External bridge network - name: ${LOCAL_NETWORK_NAME} - external: true - stack_network: # Stack network - - -services: - - nxmeta: - image: docker.io/ptr727/nxmeta-lsio:latest - container_name: nxmeta - hostname: nxmeta - domainname: ${DOMAIN_NAME} - restart: unless-stopped - user: root - group_add: - - ${DOCKER_GROUP_ID} - security_opt: # Set with care - - seccomp=unconfined - - apparmor=unconfined - devices: # Pass through Intel/AMD iGPU for hardware accelerated video - - /dev/dri - environment: - - TZ=${TZ} - - PUID=${USER_NONROOT_ID} # Run as non-root user - - PGID=${USERS_GROUP_ID} - volumes: # ZFS volumes - - ${APPDATA_DIR}/nxmeta/config:/config - - ${NVR_DIR}/media:/media # ssdpool/nvr-media - - ${NVR_DIR}/backup:/backup # hddpool/nvr-backup - - ${NVR_DIR}/analytics:/analytics # ssdpool/nvr-analytics - tmpfs: # Keep mediaserver's Unix socket and tmp files in RAM - - /tmp:size=1g,mode=1777 - networks: - public_network: - ipv4_address: ${NXMETA_IP} # Static IP - mac_address: ${NXMETA_MAC} # Static MAC - local_network: - stack_network: - labels: - - traefik.enable=true # Traefik SSL proxy - # Two-hostname pattern (DNS records configured externally, e.g. - # in your LAN DNS or hosts file): an A/AAAA `nxmeta -> ${NXMETA_IP}` - # points at the dedicated macvlan IP above for direct access, - # and a CNAME `nxmeta-web -> <traefik-host>` fronts the service - # with SSL termination. Traefik routes only the `-web` hostname; - # the bare hostname bypasses Traefik entirely. - - traefik.http.routers.nxmeta.rule=HostRegexp(`^nxmeta-web${DOMAIN_REGEX}$$`) - - traefik.http.services.nxmeta.loadbalancer.server.scheme=https - - traefik.http.services.nxmeta.loadbalancer.server.port=7001 -``` - -## Table of Contents - -- [Build and Distribution](#build-and-distribution) - - [Build Status](#build-status) - - [Release Notes](#release-notes) -- [Getting Started](#getting-started) -- [Table of Contents](#table-of-contents) -- [Products](#products) -- [Releases](#releases) -- [Overview](#overview) - - [Introduction](#introduction) - - [Base Images](#base-images) - - [LinuxServer](#linuxserver) -- [Configuration](#configuration) - - [LSIO Volumes](#lsio-volumes) - - [Non-LSIO Volumes](#non-lsio-volumes) - - [Ports](#ports) - - [Environment Variables](#environment-variables) - - [Network Mode](#network-mode) -- [Examples](#examples) - - [LSIO Docker Create](#lsio-docker-create) - - [LSIO Docker Compose](#lsio-docker-compose) - - [Non-LSIO Docker Compose](#non-lsio-docker-compose) - - [Unraid Template](#unraid-template) -- [Product Information](#product-information) - - [Release Information](#release-information) - - [Advanced Configuration](#advanced-configuration) -- [Build Process](#build-process) -- [Known Issues](#known-issues) -- [Troubleshooting](#troubleshooting) - - [Missing Storage](#missing-storage) -- [License](#license) - -## Products - -The project supports the following product variants: - -- [Network Optix][networkoptix-link] [Nx Witness VMS][nxwitness-link] (not available for purchase in the US) -- [Network Optix][networkoptix-link] [Nx Meta VMS][nxmeta-link] (developer and early access version of Nx Witness) -- [Network Optix][networkoptix-link] [Nx Go VMS][nxgo-link] (version of Nx Witness targeted at transportation sector) -- [Digital Watchdog][digitalwatchdog-link] [DW Spectrum IPVMS][dwspectrum-link] (US licensed and OEM branded version of Nx Witness) -- [Hanwha Vision][hanwhavision-link] [Wisenet WAVE VMS][dwspectrum-link] (US licensed and OEM branded version of Nx Witness) - -## Releases - -Images are published on [Docker Hub][hub-link]: - -- [NxWitness][hubnxwitness-link]: `docker pull docker.io/ptr727/nxwitness` -- [NxWitness-LSIO][hubnxwitnesslsio-link]: `docker pull docker.io/ptr727/nxwitness-lsio` -- [NxMeta][hubnxmeta-link]: `docker pull docker.io/ptr727/nxmeta` -- [NxMeta-LSIO][hubnxmetalsio-link]: `docker pull docker.io/ptr727/nxmeta-lsio` -- [NxGo][hubnxgo-link]: `docker pull docker.io/ptr727/nxgo` -- [NxGo-LSIO][hubnxwitnesslsio-link]: `docker pull docker.io/ptr727/nxgo-lsio` -- [DWSpectrum][hubdwspectrum-link]: `docker pull docker.io/ptr727/dwspectrum` -- [DWSpectrum-LSIO][hubdwspectrumlsio-link]: `docker pull docker.io/ptr727/dwspectrum-lsio` -- [WisenetWAVE][hubwisenetwave-link]: `docker pull docker.io/ptr727/wisenetwave` -- [WisenetWAVE-LSIO][hubwisenetwavelsio-link]: `docker pull docker.io/ptr727/wisenetwave-lsio` - -Images are tagged as follows: - -- `latest`: Latest published version, e.g. `docker pull docker.io/ptr727/nxmeta:latest`. -- `stable`: Latest released version, e.g. `docker pull docker.io/ptr727/nxmeta:stable`. -- `rc`: Latest RC version, e.g. `docker pull docker.io/ptr727/nxmeta:rc`. -- `beta`: Latest Beta version, e.g. `docker pull docker.io/ptr727/nxmeta:beta` -- `develop`: Builds created from the develop branch, e.g. `docker pull docker.io/ptr727/nxmeta:develop`. -- `[version]`: Release version number, e.g. `docker pull docker.io/ptr727/nxmeta:5.2.2.37996`. - -Notes: - -- `latest` and `stable` may be the same version if all builds are released builds. -- `rc` and `beta` tags are only built when RC and Beta builds are published by Nx, and may be older than current `latest` or `stable` builds. -- Images are published once a week on a schedule (and on-demand via manual trigger), picking up the latest upstream Ubuntu updates and newly released Nx product versions. A single scheduled run publishes both the `main` tags (`latest`, `stable`, version numbers) and the `develop` tags. Merging code or dependency updates does not republish images, so the published images only change when there is an actual content change. -- See [Build Process](#build-process) for more details. - -**Docker releases**: - -[NxWitness][hubnxwitness-link]:\ -[![NxWitness Stable][hubnxwitnessstable-shield]][hubnxwitness-link] -[![NxWitness Latest][hubnxwitnesslatest-shield]][hubnxwitness-link] -[![NxWitness RC][hubnxwitnessrc-shield]][hubnxwitness-link] -[![NxWitness Beta][hubnxwitnessbeta-shield]][hubnxwitness-link] - -[NxWitness-LSIO][hubnxwitnesslsio-link]:\ -[![NxWitness-LSIO Stable][hubnxwitnesslsiostable-shield]][hubnxwitnesslsio-link] -[![NxWitness-LSIO Latest][hubnxwitnesslsiolatest-shield]][hubnxwitnesslsio-link] -[![NxWitness-LSIO RC][hubnxwitnesslsiorc-shield]][hubnxwitnesslsio-link] -[![NxWitness-LSIO Beta][hubnxwitnesslsiobeta-shield]][hubnxwitnesslsio-link] - -[NxMeta][hubnxmeta-link]:\ -[![NxMeta Stable][hubnxmetastable-shield]][hubnxmeta-link] -[![NxMeta Latest][hubnxmetalatest-shield]][hubnxmeta-link] -[![NxMeta RC][hubnxmetarc-shield]][hubnxmeta-link] -[![NxMeta Beta][hubnxmetabeta-shield]][hubnxmeta-link] - -[NxMeta-LSIO][hubnxmetalsio-link]:\ -[![NxMeta-LSIO Stable][hubnxmetalsiostable-shield]][hubnxmetalsio-link] -[![NxMeta-LSIO Latest][hubnxmetalsiolatest-shield]][hubnxmetalsio-link] -[![NxMeta-LSIO RC][hubnxmetalsiorc-shield]][hubnxmetalsio-link] -[![NxMeta-LSIO Beta][hubnxmetalsiobeta-shield]][hubnxmetalsio-link] - -[NxGo][hubnxgo-link]:\ -[![NxGo Stable][hubnxgostable-shield]][hubnxgo-link] -[![NxGo Latest][hubnxgolatest-shield]][hubnxgo-link] -[![NxGo RC][hubnxgorc-shield]][hubnxgo-link] -[![NxGo Beta][hubnxgobeta-shield]][hubnxgo-link] - -[NxGo-LSIO][hubnxgolsio-link]:\ -[![NxGo-LSIO Stable][hubnxgolsiostable-shield]][hubnxgolsio-link] -[![NxGo-LSIO Latest][hubnxgolsiolatest-shield]][hubnxgolsio-link] -[![NxGo-LSIO RC][hubnxgolsiorc-shield]][hubnxgolsio-link] -[![NxGo-LSIO Beta][hubnxgolsiobeta-shield]][hubnxgolsio-link] - -[DWSpectrum][hubdwspectrum-link]:\ -[![DWSpectrum Stable][hubdwspectrumstable-shield]][hubdwspectrum-link] -[![DWSpectrum Latest][hubdwspectrumlatest-shield]][hubdwspectrum-link] -[![DWSpectrum RC][hubdwspectrumrc-shield]][hubdwspectrum-link] -[![DWSpectrum Beta][hubdwspectrumbeta-shield]][hubdwspectrum-link] - -[DWSpectrum-LSIO][hubdwspectrumlsio-link]:\ -[![DWSpectrum-LSIO Stable][hubdwspectrumlsiostable-shield]][hubdwspectrumlsio-link] -[![DWSpectrum-LSIO Latest][hubdwspectrumlsiolatest-shield]][hubdwspectrumlsio-link] -[![DWSpectrum-LSIO RC][hubdwspectrumlsiorc-shield]][hubdwspectrumlsio-link] -[![DWSpectrum-LSIO Beta][hubdwspectrumlsiobeta-shield]][hubdwspectrumlsio-link] - -[WisenetWAVE][hubwisenetwave-link]:\ -[![WisenetWAVE Stable][hubwisenetwavestable-shield]][hubwisenetwave-link] -[![WisenetWAVE Latest][hubwisenetwavelatest-shield]][hubwisenetwave-link] -[![WisenetWAVE RC][hubwisenetwaverc-shield]][hubwisenetwave-link] -[![WisenetWAVE Beta][hubwisenetwavebeta-shield]][hubwisenetwave-link] - -[WisenetWAVE-LSIO][hubwisenetwavelsio-link]:\ -[![WisenetWAVE-LSIO Stable][hubwisenetwavelsiostable-shield]][hubwisenetwavelsio-link] -[![WisenetWAVE-LSIO Latest][hubwisenetwavelsiolatest-shield]][hubwisenetwavelsio-link] -[![WisenetWAVE-LSIO RC][hubwisenetwavelsiorc-shield]][hubwisenetwavelsio-link] -[![WisenetWAVE-LSIO Beta][hubwisenetwavelsiobeta-shield]][hubwisenetwavelsio-link] - -## Overview - -### Introduction - -I ran DW Spectrum in my home lab on an Ubuntu Virtual Machine, and was looking for a way to run it in Docker. At the time Network Optix provided no support for Docker, but I did find the [The Home Repot NxWitness][thehomegithub-link] project, that inspired me to create this project.\ -I started with individual repositories for Nx Witness, Nx Meta, and DW Spectrum, but that soon became cumbersome with lots of duplication, and I combined all product flavors into this one project. - -Today Network Optix supports [Docker][nxdocker-link], and they publish [build scripts][nxgithubdocker-link], but they do not publish container images. - -### Base Images - -The project creates two variants of each product using different base images: - -- [Ubuntu][ubuntu-link] using [ubuntu:noble][ubuntudocker-link] base image. -- [LinuxServer][lsio-link] using [lsiobase/ubuntu:noble][ubuntulsiodocker-link] base image. - -The build pipeline also publishes internal base images (`ptr727/nx-base:ubuntu-noble` and -`ptr727/nx-base-lsio:ubuntu-noble`) built from `ubuntu:noble` and `lsiobase/ubuntu:noble` -to reduce CI cache churn. These base images are not intended for end-user deployments. - -Note that smaller base images like [Alpine][alpine-link] are not [supported][nxossupport-link] by the mediaserver. - -### LinuxServer - -The [LinuxServer (LSIO)][lsio-link] base images provide valuable container functionality: - -- The LSIO images are based on [s6-overlay][s6overlay-link], are updated weekly, and LSIO [produces][lsiofleet-link] containers for many popular open source applications. -- LSIO allows us to [specify][lsiopuid-link] the user account to use when running the mediaserver, while still running the `root-tool` as `root` (required for license enforcement). -- Running as non-root is a [best practice][dockernonroot-link], and required if we need user specific permissions when accessing mapped volumes. -- The [nxvms-docker][nxgithubcompose-link] project takes a different approach running a compose stack that runs the mediaserver in one instance under the `${COMPANY_NAME}` account, and the root-tool in a second instance under the `root` account, using a shared `/tmp` volume for socket IPC between the mediaserver and root-tool, but the user account `${COMPANY_NAME}` does not readily map to a user on the host system. - -## Configuration - -User accounts and directory names are based on the product variant exposed by the `${COMPANY_NAME}` variable: - -- NxWitness: `networkoptix` -- DWSpectrum: `digitalwatchdog` -- NxMeta: `networkoptix-metavms` -- WisenetWAVE: `hanwha` - -### LSIO Volumes - -The LSIO images [re-link](./LSIO/etc/s6-overlay/s6-rc.d/init-nx-relocate/run) various internal paths to `/config`. - -- `/config` : Configuration files: - - `/opt/${COMPANY_NAME}/mediaserver/etc` links to `/config/etc` : Configuration. - - `/root/.config/nx_ini` links to `/config/ini` : Additional configuration. - - `/opt/${COMPANY_NAME}/mediaserver/var` links to `/config/var` : State and logs. -- `/media` : Recording files. - -### Non-LSIO Volumes - -The non-LSIO images must be mapped directly to the installed paths, refer to the [nxvms-docker][nxgithubvolumes-link] page for details. - -- `/opt/${COMPANY_NAME}/mediaserver/etc` : Configuration. -- `/home/${COMPANY_NAME}/.config/nx_ini` : Additional configuration. -- `/opt/${COMPANY_NAME}/mediaserver/var` : State and logs. -- `/media` : Recording files. - -### Ports - -- `7001` : Default server port. - -### Environment Variables - -- `PUID` : User Id, LSIO only, optional. -- `PGID` : Group Id, LSIO only, optional. -- `TZ` : Timezone, e.g. `America/Los_Angeles`. - -See [LSIO docs][lsiopuid-link] for usage of `PUID` and `PGID` that allow the mediaserver to run under a user account and the root-tool to run as root. - -### Network Mode - -Any network mode can be used, but due to the hardware bound licensing, `host` mode is [recommended][nxgithubnetworking-link]. - -## Examples - -### LSIO Docker Create - -```shell -docker create \ - --name=nxwitness-lsio-test-container \ - --hostname=nxwitness-lsio-test-host \ - --domainname=foo.bar.net \ - --restart=unless-stopped \ - --network=host \ - --env TZ=America/Los_Angeles \ - --volume /mnt/nxwitness/config:/config:rw \ - --volume /mnt/nxwitness/media:/media:rw \ - docker.io/ptr727/nxwitness-lsio:stable - -docker start nxwitness-lsio-test-container -``` - -### LSIO Docker Compose - -```yaml -services: - nxwitness: - image: docker.io/ptr727/nxwitness-lsio:stable - container_name: nxwitness-lsio-test-container - restart: unless-stopped - network_mode: host - environment: - # - PUID=65534 # id $user - # - PGID=65534 # id $group - - TZ=America/Los_Angeles - volumes: - - /mnt/nxwitness/config:/config - - /mnt/nxwitness/media:/media - tmpfs: - # Keep mediaserver's Unix socket and tmp files in RAM - - /tmp:size=1g,mode=1777 -``` - -### Non-LSIO Docker Compose - -```yaml -services: - nxwitness: - image: docker.io/ptr727/nxwitness:stable - container_name: nxwitness-test-container - restart: unless-stopped - network_mode: host - volumes: - - /mnt/nxwitness/config/etc:/opt/networkoptix/mediaserver/etc - - /mnt/nxwitness/config/nx_ini:/home/networkoptix/.config/nx_ini - - /mnt/nxwitness/config/var:/opt/networkoptix/mediaserver/var - - /mnt/nxwitness/media:/media - tmpfs: - # Keep mediaserver's Unix socket and tmp files in RAM - - /tmp:size=1g,mode=1777 -``` - -### Unraid Template - -- Add the template [URL](./Unraid) `https://github.com/ptr727/NxWitness/tree/main/Unraid` to the "Template Repositories" section, at the bottom of the "Docker" configuration tab, and click "Save". -- Create a new container by clicking the "Add Container" button, select the desired product template from the dropdown. -- If using Unassigned Devices for media storage, use `RW/Slave` access mode. -- Use `nobody` and `users` identifiers, `PUID=99` and `PGID=100`. -- Register the Unraid filesystems in the `additionalLocalFsTypes` advanced settings, see the [Missing Storage](#missing-storage) section for help. - -## Product Information - -### Release Information - -- Nx Witness: - - [Releases JSON API][nxwitnessreleases-link] - - [Downloads][nxwitnessdownload-link] - - [Beta Downloads][nxwitnessbetadownload-link] - - [Release Notes][nxwitnessreleasenotes-link] -- Nx Meta: - - [Releases JSON API][nxmetareleases-link] - - [Signup for Nx Meta][getstartedwithmeta-link] - - [Request Developer Licenses][getalicense-link] - - [Downloads][nxmetadownload-link] - - [Beta Downloads][nxmetabetadownload-link] -- Nx Go: - - [Releases JSON API][nxgoreleases-link] - - [Downloads][nxgodownload-link] - - [Beta Downloads][nxgobetadownload-link] - - [Release Notes][nxgoreleasenotes-link] -- DW Spectrum: - - [Releases JSON API][dwspectrumreleases-link] - - [Downloads][dwspectrumdownload-link] - - [Release Notes][dwspectrumreleasenotes-link] -- Wisenet WAVE: - - [Releases JSON API][wisenetwavereleases-link] - - [Downloads][wisenetwavedownload-link] - - [Release Notes][wisenetwavereleasenotes-link] - -### Advanced Configuration - -- `mediaserver.conf` [Configuration][configoptions-link]: `https://[hostname]:[port]/#/server-documentation` -- `nx_vms_server.ini` [Configuration][iniconfig-link]: `https://[hostname]:[port]/api/iniConfig/` -- Advanced Server Configuration: `https://[hostname]:[port]/#/settings/advanced` -- Storage Reporting: `https://[hostname]:[port]/#/health/storages` - -## Build Process - -**Build overview**: - -- [`CreateMatrix`](./CreateMatrix/) is used to update available product versions, and to create Docker files for all product permutations. -- [`Version.json`](./Make/Version.json) is updated using the mediaserver [Releases JSON API][nxwitnessreleases-link] and [Packages API][packages-link]. -- The logic follows the same pattern as used by the [Nx Open][releaseinfo-link] desktop client logic. -- The "released" status of a build follows the same method as Nx uses in [`isBuildPublished()`][isbuildpublished-link] where `release_date` and `release_delivery_days` from the [Releases JSON API][nxwitnessreleases-link] must be greater than `0` -- [`Matrix.json`](./Make/Matrix.json) is created from the `Version.json` file and is used during pipeline builds using a [Matrix][matrix-link] strategy. -- Automated builds use [GitHub Actions](https://docs.github.com/en/actions): - - Pull requests run unit tests, and when image files change, a fast representative amd64 smoke build of `NxMeta` and `NxMeta-LSIO` ([`test-pull-request.yml`](./.github/workflows/test-pull-request.yml)) -- the full matrix is not built on every PR. - - Publishing happens only on a weekly schedule or manual trigger ([`publish-release.yml`](./.github/workflows/publish-release.yml)), which builds and pushes the full matrix for both the `main` and `develop` branches. Merges to `main`/`develop` (including auto-merged Dependabot and codegen updates) do not publish; the next scheduled run picks them up. -- Version history is maintained and used by `CreateMatrix` such that generic tags, e.g. `latest`, will never result in a lesser version number, i.e. break-fix-forward only, see [Issue #62](https://github.com/ptr727/NxWitness/issues/62) for details on Nx re-publishing "released" builds using an older version breaking already upgraded systems. - -**Local testing**: - -- Run `cd ./Make` and [`./Test.sh`](./Make/Test.sh), the following will be executed: - - [`Create.sh`](./Make/Create.sh): Create `Dockerfile`'s and update the latest version information using `CreateMatrix`. - - [`Build.sh`](./Make/Build.sh): Builds the `Dockerfile`'s using `docker buildx build`. - - [`Up.sh`](./Make/Up.sh): Launch a docker compose stack [`Test.yaml`](./Make/Test.yml) to run all product variants. -- Ctrl-Click on the links to launch the web UI for each of the product variants. -- Run [`Clean.sh`](./Make/Clean.sh) to shutdown the compose stack and cleanup images. - -## Known Issues - -- Licensing: - - Camera recording license keys are activated and bound to hardware attributes of the host server collected by the `root-tool` that is required to run as `root`. - - Requiring the `root-tool` to run as root overly complicates running the `mediaserver` as a non-root user, and requires the container to run using `host` networking to not break the hardware license checks. - - Docker containers are supposed to be portable, and moving containers between hosts will break license activation. - - Nx's own [`nxvms-docker`][nxgithubcompose-link] reference image [disabled `root-tool` in late 2025](https://github.com/networkoptix/nxvms-docker/commit/4285f93) by setting `ignoreRootTool=true` in `mediaserver.conf` and dropping their separate `root-tool` container. This trades hardware-ID license enforcement for a simpler unprivileged container. NxWitness does **not** follow this change - licensed deployments would lose activation - and will revisit only if Nx publishes a clearer official position on Docker licensing without `root-tool`. - - Nx to fix: Associate licenses with the [Cloud Account][nxcloud-link] not the local hardware. -- Storage Management: - - The mediaserver attempts to automatically decide what storage to use. - - Filesystem types are filtered out if not on the [supported list][nxgithubstorage-link]. - - Mounted volumes are ignored if backed by the same physical storage, even if logically separate. - - Unwanted `Nx MetaVMS Media` directories are created on any discoverable writable storage. - - Nx to fix: Eliminate the elaborate filesystem filter logic and use only the admin specified storage locations. -- Configuration Files: - - `.conf` configuration files are located in a static `mediaserver/etc` location while `.ini` configuration files are in a user-account dependent location, e.g. `/home/networkoptix/.config/nx_ini` or `/root/.config/nx_ini`. - - There is no value in having a server use per-user configuration directories, and it is inconsistent to mix configuration file locations. - - Nx to fix: Store all configuration files in `mediaserver/etc`. -- External Plugins: - - Custom or [Marketplace][nxmarketplace-link] plugins are installed in the `mediaserver/bin/plugins` directory. - - The `mediaserver/bin/plugins` directory is already pre-populated with Nx installed plugins. - - It is not possible to use external plugins from a mounted volume as the directory is already in-use. - - Nx to fix: Load plugins from `mediaserver/var/plugins` or from sub-directories mounted below `mediaserver/bin/plugins`, e.g. `mediaserver/bin/plugins/external` -- Lifetime Upgrades: - - Nx is a cloud product, free to view, free upgrades, comes with ongoing costs of hosting, maintenance, and support, it is [unfeasible][nxcrunchbase-link] to sustain a business with ongoing costs using perpetual one-off licenses. - - My personal experience with [Digital Watchdog][digitalwatchdog-link] and their [Lifetime Upgrades and No Annual Agreements][dwupgrades-link] is an inflexible policy of three activations per license and you have to buy a new license, thus the "license lifetime" is a multiplier of the "hardware lifetime". - - Nx to fix: Yearly camera license renewals covering the cost of support and upgrades. -- Archiving: - - Nx makes no distinction between recording and archiving storage, archive is basically just a recording mirror without any capacity or retention benefit. - - Recording storage is typically high speed low latency high cost low capacity SSD/NVMe arrays, while archival playback storage is very high capacity low cost magnetic media arrays. - - Nx to fix: Implement something akin to archiving in [Milestone XProtect VMS][milestone-link] where recording storage is separate from long term archival storage. -- Image Publication: - - Nx relies on end-users or projects like this one to create and publish docker images. - - Nx to fix: Publish up-to-date images for all product variants and release channels. -- Break-Fix-Version-Forward: - - Nx product versions published via their releases API occasionally go backwards, e.g. `release`: v4.3 -> v5.0 -> v4.3. - - Nx supports forward-only in-place upgrades, e.g. v4.3 to v5.0, but not v5.0 to v4.3. - - Publishing generic tags, e.g. `latest`, using a version that regresses, e.g. v4.3 -> v5.0 -> v4.3 breaks deployments, see [Issue #62](https://github.com/ptr727/NxWitness/issues/62) for details. - - `CreateMatrix` tooling keeps track of published versions, and prevents version regression of generic `latest`, `rc` and `beta` tags. - - Nx to fix: Release break-fix-version-forward only via release API's. - -## Troubleshooting - -I am not affiliated with Network Optix, I cannot provide support for their products, please contact [Network Optix Support][nxsupport-link] for product support issues.\ -If there are issues with the docker build scripts used in this project, please create a [GitHub Issue][issues-link].\ -Note that I only test and run `nxmeta-lsio:stable` in my home lab, other images get very little to no testing, please test accordingly. - -### Missing Storage - -The following section will help troubleshoot common problems with missing storage.\ -If this does not help, please contact [Network Optix Support][nxsupport-link].\ -Please do not open a GitHub issue unless you are positive the issue is with the `Dockerfile`. - -Confirm that all the mounted volumes are listed in the available storage locations in the [web admin][nxwebadmin-link] portal. - -Enable [debug logging][nxdebuglogging-link] in the mediaserver:\ -Edit `mediaserver.conf`, set `logLevel=verbose`, restart the server.\ -Look for clues in `/config/var/log/log_file.log`. - -E.g. - -```log -VERBOSE nx::vms::server::fs: shfs /media fuse.shfs - duplicate -VERBOSE nx::vms::server::fs: /dev/sdb8 /media btrfs - duplicate -DEBUG QnStorageSpaceRestHandler(0x7f85043b0b00): Return 0 storages and 1 protocols -``` - -Get a list of the mapped volume mounts in the running container, and verify that `/config` and `/media` are listed in the `Mounts` section: - -```shell -docker ps --no-trunc -docker container inspect [containername] -``` - -Launch a shell in the running container and get a list of filesystems mounts: - -```shell -docker ps --no-trunc -docker exec --interactive --tty [containername] /bin/bash -cat /proc/mounts -exit -``` - -Example output for ZFS (note that ZFS support [was added][nxreleasenotes-link] in v5.0): - -```shell -ssdpool/appdata /config zfs rw,noatime,xattr,posixacl 0 0 -nvrpool/nvr /media zfs rw,noatime,xattr,posixacl 0 0 -ssdpool/docker /archive zfs rw,noatime,xattr,posixacl 0 0 -``` - -Mount `/config` is on device `ssdpool/appdata` and filesystem is `zfs`.\ -Mount `/media` is on device `nvrpool/nvr` and filesystem is `zfs`.\ -Mount `/archive` is on device `ssdpool/docker` and filesystem is `zfs`. - -In this case the devices are unique and will not be filtered, but `zfs` is not supported and needs to be registered. - -Example output for UnRaid FUSE: - -```shell -shfs /config fuse.shfs rw,nosuid,nodev,noatime,user_id=0,group_id=0,allow_other 0 0 -shfs /media fuse.shfs rw,nosuid,nodev,noatime,user_id=0,group_id=0,allow_other 0 0 -shfs /archive fuse.shfs rw,nosuid,nodev,noatime,user_id=0,group_id=0,allow_other 0 0 -``` - -In this case there are two issues, the device is `/shfs` for all three mounts and will be filtered, and the filesystem type is `fuse.shfs` that is not supported and needs to be registered. - -Log file output for Unraid FUSE: - -```log -VERBOSE nx::vms::server::fs: shfs /config fuse.shfs - added -VERBOSE nx::vms::server::fs: shfs /media fuse.shfs - added -VERBOSE nx::vms::server::fs: shfs /archive fuse.shfs - duplicate -``` - -The `/archive` mount is classified as a duplicate and ignored, map just `/media`, do not map `/archive`.\ -Alternative use the "Unassigned Devices" plugin and dedicate e.g. a XFS formatted SSD drive to `/media` and/or `/config`. - -Example output for Unraid BTRFS: - -```shell -/dev/sdb8 /test btrfs rw,relatime,space_cache,subvolid=5,subvol=/test 0 0 -/dev/sdb8 /config btrfs rw,relatime,space_cache,subvolid=5,subvol=/config 0 0 -/dev/sdb8 /media btrfs rw,relatime,space_cache,subvolid=5,subvol=/media 0 0 -/dev/sdb8 /archive btrfs rw,relatime,space_cache,subvolid=5,subvol=/archive 0 0 -``` - -```log -VERBOSE nx::vms::server::fs: /dev/sdb8 /test btrfs - added -VERBOSE nx::vms::server::fs: /dev/sdb8 /config btrfs - duplicate -VERBOSE nx::vms::server::fs: /dev/sdb8 /media btrfs - duplicate -VERBOSE nx::vms::server::fs: /dev/sdb8 /archive btrfs - duplicate -``` - -In this example the `/test` volume was accepted, but all other volumes on `/dev/sdb8` was ignored as duplicates. - -Add the required filesystem types in the [advanced configuration](#advanced-configuration) menu. -Edit the `additionalLocalFsTypes` option and add the required filesystem types, e.g. `fuse.shfs,btrfs,zfs`, restart the server. - -Alternatively call the configuration API directly:\ -`wget --no-check-certificate --user=[username] --password=[password] https://[hostname]:[port]/api/systemSettings?additionalLocalFsTypes=fuse.shfs,btrfs,zfs`. - -To my knowledge there is no solution to duplicate devices being filtered, please contact [Network Optix Support][nxsupport-link] and ask them to stop filtering filesystem types and devices. - -## License - -Licensed under the [MIT License][license-link]\ -![GitHub License][license-shield] - -[actions-link]: https://github.com/ptr727/NxWitness/actions -[alpine-link]: https://alpinelinux.org/ -[configoptions-link]: https://support.networkoptix.com/hc/en-us/articles/360036389693-How-to-access-Nx-Server-configuration-options -[digitalwatchdog-link]: https://digital-watchdog.com/ -[dockernonroot-link]: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user -[dwspectrum-link]: https://dwspectrum.com/ -[dwspectrumdownload-link]: https://dwspectrum.digital-watchdog.com/download/linux -[dwspectrumreleasenotes-link]: https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html -[dwspectrumreleases-link]: https://updates.vmsproxy.com/digitalwatchdog/releases.json -[dwupgrades-link]: https://dwspectrum.com/upgrades/ -[getalicense-link]: https://support.networkoptix.com/hc/en-us/articles/8693698259607-Get-a-License-for-Developers -[getstartedwithmeta-link]: https://www.networkoptix.com/nx-meta/get-started-with-meta -[github-link]: https://github.com/ptr727/NxWitness -[hanwhavision-link]: https://hanwhavisionamerica.com/ -[hub-link]: https://hub.docker.com/u/ptr727 -[hubdwspectrum-link]: https://hub.docker.com/r/ptr727/dwspectrum -[hubdwspectrumbeta-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum/beta?label=beta&logo=docker -[hubdwspectrumlatest-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum/latest?label=latest&logo=docker -[hubdwspectrumlsio-link]: https://hub.docker.com/r/ptr727/dwspectrum-lsio -[hubdwspectrumlsiobeta-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum-lsio/beta?label=beta&logo=docker -[hubdwspectrumlsiolatest-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum-lsio/latest?label=latest&logo=docker -[hubdwspectrumlsiorc-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum-lsio/rc?label=rc&logo=docker -[hubdwspectrumlsiostable-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum-lsio/stable?label=stable&logo=docker -[hubdwspectrumrc-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum/rc?label=rc&logo=docker -[hubdwspectrumstable-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum/stable?label=stable&logo=docker -[hubnxgo-link]: https://hub.docker.com/r/ptr727/nxgo -[hubnxgobeta-shield]: https://img.shields.io/docker/v/ptr727/nxgo/beta?label=beta&logo=docker -[hubnxgolatest-shield]: https://img.shields.io/docker/v/ptr727/nxgo/latest?label=latest&logo=docker -[hubnxgolsio-link]: https://hub.docker.com/r/ptr727/nxgo-lsio -[hubnxgolsiobeta-shield]: https://img.shields.io/docker/v/ptr727/nxgo-lsio/beta?label=beta&logo=docker -[hubnxgolsiolatest-shield]: https://img.shields.io/docker/v/ptr727/nxgo-lsio/latest?label=latest&logo=docker -[hubnxgolsiorc-shield]: https://img.shields.io/docker/v/ptr727/nxgo-lsio/rc?label=rc&logo=docker -[hubnxgolsiostable-shield]: https://img.shields.io/docker/v/ptr727/nxgo-lsio/stable?label=stable&logo=docker -[hubnxgorc-shield]: https://img.shields.io/docker/v/ptr727/nxgo/rc?label=rc&logo=docker -[hubnxgostable-shield]: https://img.shields.io/docker/v/ptr727/nxgo/stable?label=stable&logo=docker -[hubnxmeta-link]: https://hub.docker.com/r/ptr727/nxmeta -[hubnxmetabeta-shield]: https://img.shields.io/docker/v/ptr727/nxmeta/beta?label=beta&logo=docker -[hubnxmetalatest-shield]: https://img.shields.io/docker/v/ptr727/nxmeta/latest?label=latest&logo=docker -[hubnxmetalsio-link]: https://hub.docker.com/r/ptr727/nxmeta-lsio -[hubnxmetalsiobeta-shield]: https://img.shields.io/docker/v/ptr727/nxmeta-lsio/beta?label=beta&logo=docker -[hubnxmetalsiolatest-shield]: https://img.shields.io/docker/v/ptr727/nxmeta-lsio/latest?label=latest&logo=docker -[hubnxmetalsiorc-shield]: https://img.shields.io/docker/v/ptr727/nxmeta-lsio/rc?label=rc&logo=docker -[hubnxmetalsiostable-shield]: https://img.shields.io/docker/v/ptr727/nxmeta-lsio/stable?label=stable&logo=docker -[hubnxmetarc-shield]: https://img.shields.io/docker/v/ptr727/nxmeta/rc?label=rc&logo=docker -[hubnxmetastable-shield]: https://img.shields.io/docker/v/ptr727/nxmeta/stable?label=stable&logo=docker -[hubnxwitness-link]: https://hub.docker.com/r/ptr727/nxwitness -[hubnxwitnessbeta-shield]: https://img.shields.io/docker/v/ptr727/nxwitness/beta?label=beta&logo=docker -[hubnxwitnesslatest-shield]: https://img.shields.io/docker/v/ptr727/nxwitness/latest?label=latest&logo=docker -[hubnxwitnesslsio-link]: https://hub.docker.com/r/ptr727/nxwitness-lsio -[hubnxwitnesslsiobeta-shield]: https://img.shields.io/docker/v/ptr727/nxwitness-lsio/beta?label=beta&logo=docker -[hubnxwitnesslsiolatest-shield]: https://img.shields.io/docker/v/ptr727/nxwitness-lsio/latest?label=latest&logo=docker -[hubnxwitnesslsiorc-shield]: https://img.shields.io/docker/v/ptr727/nxwitness-lsio/rc?label=rc&logo=docker -[hubnxwitnesslsiostable-shield]: https://img.shields.io/docker/v/ptr727/nxwitness-lsio/stable?label=stable&logo=docker -[hubnxwitnessrc-shield]: https://img.shields.io/docker/v/ptr727/nxwitness/rc?label=rc&logo=docker -[hubnxwitnessstable-shield]: https://img.shields.io/docker/v/ptr727/nxwitness/stable?label=stable&logo=docker -[hubwisenetwave-link]: https://hub.docker.com/r/ptr727/wisenetwave -[hubwisenetwavebeta-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave/beta?label=beta&logo=docker -[hubwisenetwavelatest-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave/latest?label=latest&logo=docker -[hubwisenetwavelsio-link]: https://hub.docker.com/r/ptr727/wisenetwave-lsio -[hubwisenetwavelsiobeta-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave-lsio/beta?label=beta&logo=docker -[hubwisenetwavelsiolatest-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave-lsio/latest?label=latest&logo=docker -[hubwisenetwavelsiorc-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave-lsio/rc?label=rc&logo=docker -[hubwisenetwavelsiostable-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave-lsio/stable?label=stable&logo=docker -[hubwisenetwaverc-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave/rc?label=rc&logo=docker -[hubwisenetwavestable-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave/stable?label=stable&logo=docker -[iniconfig-link]: https://meta.nxvms.com/docs/developers/knowledgebase/241-configuring-via-ini-files--iniconfig -[isbuildpublished-link]: https://github.com/networkoptix/nx_open/blob/526967920636d3119c92a5220290ecc10957bf12/vms/libs/nx_vms_update/src/nx/vms/update/releases_info.cpp#L31 -[issues-link]: https://github.com/ptr727/NxWitness/issues -[lastcommit-shield]: https://img.shields.io/github/last-commit/ptr727/NxWitness?logo=github&label=Last%20Commit -[license-link]: ./LICENSE -[license-shield]: https://img.shields.io/github/license/ptr727/NxWitness -[lsio-link]: https://www.linuxserver.io/ -[lsiofleet-link]: https://fleet.linuxserver.io/ -[lsiopuid-link]: https://docs.linuxserver.io/general/understanding-puid-and-pgid -[matrix-link]: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs -[milestone-link]: https://doc.milestonesys.com/latest/en-US/standard_features/sf_mc/sf_systemoverview/mc_storageandarchivingexplained.htm -[networkoptix-link]: https://www.networkoptix.com/ -[nxcloud-link]: https://www.networkoptix.com/nx-witness/nx-witness-cloud/ -[nxcrunchbase-link]: https://www.crunchbase.com/organization/network-optix -[nxdebuglogging-link]: https://support.networkoptix.com/hc/en-us/articles/236033688-How-to-change-software-logging-level-and-how-to-get-logs -[nxdocker-link]: https://support.networkoptix.com/hc/en-us/articles/360037973573-Docker -[nxgithubcompose-link]: https://github.com/networkoptix/nxvms-docker/blob/master/docker-compose.yaml -[nxgithubdocker-link]: https://github.com/networkoptix/nxvms-docker -[nxgithubnetworking-link]: https://github.com/networkoptix/nxvms-docker#networking -[nxgithubstorage-link]: https://github.com/networkoptix/nxvms-docker#notes-about-storage -[nxgithubvolumes-link]: https://github.com/networkoptix/nxvms-docker#volumes-description -[nxgo-link]: https://updates.networkoptix.com/nxgo -[nxgobetadownload-link]: https://cloud.nxgo.io/download/betas/linux -[nxgodownload-link]: https://cloud.nxgo.io/download/releases/linux -[nxgoreleasenotes-link]: https://updates.networkoptix.com/nxgo/#releases_list -[nxgoreleases-link]: https://updates.networkoptix.com/nxgo/releases.json -[nxmarketplace-link]: https://www.networkoptix.com/nx-meta/nx-integrations-marketplace/ -[nxmeta-link]: https://meta.nxvms.com/ -[nxmetabetadownload-link]: https://meta.nxvms.com/downloads/betas -[nxmetadownload-link]: https://meta.nxvms.com/download/linux -[nxmetareleases-link]: https://updates.vmsproxy.com/metavms/releases.json -[nxossupport-link]: https://support.networkoptix.com/hc/en-us/articles/205313168-Nx-Witness-Operating-System-Support -[nxreleasenotes-link]: https://support.networkoptix.com/hc/en-us/articles/360042751193-Current-and-Past-Releases-Downloads-Release-Notes -[nxsupport-link]: https://support.networkoptix.com/hc/en-us/community/topics -[nxwebadmin-link]: https://support.networkoptix.com/hc/en-us/articles/115012831028-Nx-Server-Web-Admin -[nxwitness-link]: https://www.networkoptix.com/nx-witness/ -[nxwitnessbetadownload-link]: https://beta.networkoptix.com/beta-builds/default -[nxwitnessdownload-link]: https://nxvms.com/download/linux -[nxwitnessreleasenotes-link]: https://www.networkoptix.com/all-nx-witness-release-notes -[nxwitnessreleases-link]: https://updates.vmsproxy.com/default/releases.json -[packages-link]: https://updates.networkoptix.com/default/38363/packages.json -[releasebuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/NxWitness/publish-release.yml?branch=main&logo=github&label=Build%20Status -[releaseinfo-link]: https://github.com/networkoptix/nx_open/blob/master/vms/libs/nx_vms_update/src/nx/vms/update/releases_info.cpp -[s6overlay-link]: https://github.com/just-containers/s6-overlay -[thehomegithub-link]: https://github.com/thehomerepot/nxwitness -[ubuntu-link]: https://ubuntu.com/ -[ubuntudocker-link]: https://hub.docker.com/_/ubuntu -[ubuntulsiodocker-link]: https://hub.docker.com/r/lsiobase/ubuntu -[wisenetwavedownload-link]: https://wavevms.com/download/linux -[wisenetwavereleasenotes-link]: https://wavevms.com/release-notes/ -[wisenetwavereleases-link]: https://updates.vmsproxy.com/hanwha/releases.json +# NxWitness + +Docker images for Network Optix Nx Witness and OEM-branded VMS products. + +This is a project to build and publish Docker images for various [Network Optix][networkoptix-link] VMS products, in an Ubuntu and a [LinuxServer][lsio-link] base image variant of each product. + +## Build and Distribution + +- **Source Code**: [GitHub][github-link] for source, issues, and CI/CD pipelines. +- **Versioned Releases**: [GitHub Releases][releases-link] for version-tagged source archives. +- **Docker Images**: [Docker Hub][hub-link] for the published container images. + +### Build Status + +[![Release Status][releasebuildstatus-shield]][actions-link]\ +[![Last Commit][lastcommit-shield]][github-link] + +### Releases + +[![GitHub Release][github-release-shield]][releases-link]\ +[![GitHub Pre-Release][github-pre-release-shield]][releases-link] + +**Docker releases**: + +[NxWitness][nxwitness-docker-hub-link]:\ +[![NxWitness Stable][hubnxwitnessstable-shield]][nxwitness-docker-hub-link] +[![NxWitness Latest][hubnxwitnesslatest-shield]][nxwitness-docker-hub-link] +[![NxWitness RC][hubnxwitnessrc-shield]][nxwitness-docker-hub-link] +[![NxWitness Beta][hubnxwitnessbeta-shield]][nxwitness-docker-hub-link] + +[NxWitness-LSIO][nxwitness-lsio-docker-hub-link]:\ +[![NxWitness-LSIO Stable][hubnxwitnesslsiostable-shield]][nxwitness-lsio-docker-hub-link] +[![NxWitness-LSIO Latest][hubnxwitnesslsiolatest-shield]][nxwitness-lsio-docker-hub-link] +[![NxWitness-LSIO RC][hubnxwitnesslsiorc-shield]][nxwitness-lsio-docker-hub-link] +[![NxWitness-LSIO Beta][hubnxwitnesslsiobeta-shield]][nxwitness-lsio-docker-hub-link] + +[NxMeta][nxmeta-docker-hub-link]:\ +[![NxMeta Stable][hubnxmetastable-shield]][nxmeta-docker-hub-link] +[![NxMeta Latest][hubnxmetalatest-shield]][nxmeta-docker-hub-link] +[![NxMeta RC][hubnxmetarc-shield]][nxmeta-docker-hub-link] +[![NxMeta Beta][hubnxmetabeta-shield]][nxmeta-docker-hub-link] + +[NxMeta-LSIO][nxmeta-lsio-docker-hub-link]:\ +[![NxMeta-LSIO Stable][hubnxmetalsiostable-shield]][nxmeta-lsio-docker-hub-link] +[![NxMeta-LSIO Latest][hubnxmetalsiolatest-shield]][nxmeta-lsio-docker-hub-link] +[![NxMeta-LSIO RC][hubnxmetalsiorc-shield]][nxmeta-lsio-docker-hub-link] +[![NxMeta-LSIO Beta][hubnxmetalsiobeta-shield]][nxmeta-lsio-docker-hub-link] + +[NxGo][nxgo-docker-hub-link]:\ +[![NxGo Stable][hubnxgostable-shield]][nxgo-docker-hub-link] +[![NxGo Latest][hubnxgolatest-shield]][nxgo-docker-hub-link] +[![NxGo RC][hubnxgorc-shield]][nxgo-docker-hub-link] +[![NxGo Beta][hubnxgobeta-shield]][nxgo-docker-hub-link] + +[NxGo-LSIO][nxgo-lsio-docker-hub-link]:\ +[![NxGo-LSIO Stable][hubnxgolsiostable-shield]][nxgo-lsio-docker-hub-link] +[![NxGo-LSIO Latest][hubnxgolsiolatest-shield]][nxgo-lsio-docker-hub-link] +[![NxGo-LSIO RC][hubnxgolsiorc-shield]][nxgo-lsio-docker-hub-link] +[![NxGo-LSIO Beta][hubnxgolsiobeta-shield]][nxgo-lsio-docker-hub-link] + +[DWSpectrum][dwspectrum-docker-hub-link]:\ +[![DWSpectrum Stable][hubdwspectrumstable-shield]][dwspectrum-docker-hub-link] +[![DWSpectrum Latest][hubdwspectrumlatest-shield]][dwspectrum-docker-hub-link] +[![DWSpectrum RC][hubdwspectrumrc-shield]][dwspectrum-docker-hub-link] +[![DWSpectrum Beta][hubdwspectrumbeta-shield]][dwspectrum-docker-hub-link] + +[DWSpectrum-LSIO][dwspectrum-lsio-docker-hub-link]:\ +[![DWSpectrum-LSIO Stable][hubdwspectrumlsiostable-shield]][dwspectrum-lsio-docker-hub-link] +[![DWSpectrum-LSIO Latest][hubdwspectrumlsiolatest-shield]][dwspectrum-lsio-docker-hub-link] +[![DWSpectrum-LSIO RC][hubdwspectrumlsiorc-shield]][dwspectrum-lsio-docker-hub-link] +[![DWSpectrum-LSIO Beta][hubdwspectrumlsiobeta-shield]][dwspectrum-lsio-docker-hub-link] + +[WisenetWAVE][wisenetwave-docker-hub-link]:\ +[![WisenetWAVE Stable][hubwisenetwavestable-shield]][wisenetwave-docker-hub-link] +[![WisenetWAVE Latest][hubwisenetwavelatest-shield]][wisenetwave-docker-hub-link] +[![WisenetWAVE RC][hubwisenetwaverc-shield]][wisenetwave-docker-hub-link] +[![WisenetWAVE Beta][hubwisenetwavebeta-shield]][wisenetwave-docker-hub-link] + +[WisenetWAVE-LSIO][wisenetwave-lsio-docker-hub-link]:\ +[![WisenetWAVE-LSIO Stable][hubwisenetwavelsiostable-shield]][wisenetwave-lsio-docker-hub-link] +[![WisenetWAVE-LSIO Latest][hubwisenetwavelsiolatest-shield]][wisenetwave-lsio-docker-hub-link] +[![WisenetWAVE-LSIO RC][hubwisenetwavelsiorc-shield]][wisenetwave-lsio-docker-hub-link] +[![WisenetWAVE-LSIO Beta][hubwisenetwavelsiobeta-shield]][wisenetwave-lsio-docker-hub-link] + +### Release Notes + +**Version: 2.15**: + +**Summary**: + +- Build, CI, and repository tooling changes only. No functional change to the published images. + +See [Release History][history] for complete release notes and older versions. + +## Getting Started + +**Getting started with a simple test compose file**: + +```yaml +# compose.yaml + +# Test using non persistent docker volumes +volumes: + test_nxwitness-lsio_config: + test_nxwitness-lsio_media: + test_nxwitness-lsio_backup: + test_nxwitness-lsio_analytics: + +services: + nxwitness-lsio: + # Use the image matching your product + image: docker.io/ptr727/nxwitness-lsio:stable + container_name: nxwitness-lsio-test-container + restart: unless-stopped + network_mode: bridge + ports: + # Expose the service on port 7203 + - 7203:7001 + environment: + - TZ=America/Los_Angeles + volumes: + # Map to your real storage in production + - test_nxwitness-lsio_config:/config + - test_nxwitness-lsio_media:/media + - test_nxwitness-lsio_backup:/backup + - test_nxwitness-lsio_analytics:/analytics + tmpfs: + # Keep mediaserver's Unix socket and tmp files in RAM + - /tmp:size=1g,mode=1777 +``` + +```shell +# Launch the service +docker compose up --detach + +# Open your web browser on the local machine port 7203 +echo "Nx Witness LSIO:" "https://$HOSTNAME:7203/" + +# Shut the service down +docker compose down +``` + +**Example of a service in production**: + +```yaml +networks: + + public_network: # External macvlan network + name: ${PUBLIC_NETWORK_NAME} + external: true + local_network: # External bridge network + name: ${LOCAL_NETWORK_NAME} + external: true + stack_network: # Stack network + + +services: + + nxmeta: + image: docker.io/ptr727/nxmeta-lsio:latest + container_name: nxmeta + hostname: nxmeta + domainname: ${DOMAIN_NAME} + restart: unless-stopped + user: root + group_add: + - ${DOCKER_GROUP_ID} + security_opt: # Set with care + - seccomp=unconfined + - apparmor=unconfined + devices: # Pass through Intel/AMD iGPU for hardware accelerated video + - /dev/dri + environment: + - TZ=${TZ} + - PUID=${USER_NONROOT_ID} # Run as non-root user + - PGID=${USERS_GROUP_ID} + volumes: # ZFS volumes + - ${APPDATA_DIR}/nxmeta/config:/config + - ${NVR_DIR}/media:/media # ssdpool/nvr-media + - ${NVR_DIR}/backup:/backup # hddpool/nvr-backup + - ${NVR_DIR}/analytics:/analytics # ssdpool/nvr-analytics + tmpfs: # Keep mediaserver's Unix socket and tmp files in RAM + - /tmp:size=1g,mode=1777 + networks: + public_network: + ipv4_address: ${NXMETA_IP} # Static IP + mac_address: ${NXMETA_MAC} # Static MAC + local_network: + stack_network: + labels: + - traefik.enable=true # Traefik SSL proxy + # Two-hostname pattern (DNS records configured externally, e.g. + # in your LAN DNS or hosts file): an A/AAAA `nxmeta -> ${NXMETA_IP}` + # points at the dedicated macvlan IP above for direct access, + # and a CNAME `nxmeta-web -> <traefik-host>` fronts the service + # with SSL termination. Traefik routes only the `-web` hostname; + # the bare hostname bypasses Traefik entirely. + - traefik.http.routers.nxmeta.rule=HostRegexp(`^nxmeta-web${DOMAIN_REGEX}$$`) + - traefik.http.services.nxmeta.loadbalancer.server.scheme=https + - traefik.http.services.nxmeta.loadbalancer.server.port=7001 +``` + +## Table of Contents + +- [Build and Distribution](#build-and-distribution) + - [Build Status](#build-status) + - [Releases](#releases) + - [Release Notes](#release-notes) +- [Getting Started](#getting-started) +- [Table of Contents](#table-of-contents) +- [Products](#products) +- [Overview](#overview) + - [Introduction](#introduction) + - [Base Images](#base-images) + - [LinuxServer](#linuxserver) +- [Installation](#installation) +- [Configuration](#configuration) + - [LSIO Volumes](#lsio-volumes) + - [Non-LSIO Volumes](#non-lsio-volumes) + - [Ports](#ports) + - [Environment Variables](#environment-variables) + - [Network Mode](#network-mode) +- [Examples](#examples) + - [LSIO Docker Create](#lsio-docker-create) + - [LSIO Docker Compose](#lsio-docker-compose) + - [Non-LSIO Docker Compose](#non-lsio-docker-compose) + - [Unraid Template](#unraid-template) +- [Product Information](#product-information) + - [Release Information](#release-information) + - [Advanced Configuration](#advanced-configuration) +- [Build Process](#build-process) +- [Known Issues](#known-issues) +- [Troubleshooting](#troubleshooting) + - [Missing Storage](#missing-storage) +- [Questions or Issues](#questions-or-issues) +- [3rd Party Tools](#3rd-party-tools) +- [License](#license) + +## Products + +The project supports the following product variants: + +- [Network Optix][networkoptix-link] [Nx Witness VMS][nxwitness-link] (not available for purchase in the US) +- [Network Optix][networkoptix-link] [Nx Meta VMS][nxmeta-link] (developer and early access version of Nx Witness) +- [Network Optix][networkoptix-link] [Nx Go VMS][nxgo-link] (version of Nx Witness targeted at transportation sector) +- [Digital Watchdog][digitalwatchdog-link] [DW Spectrum IPVMS][dwspectrum-link] (US licensed and OEM-branded version of Nx Witness) +- [Hanwha Vision][hanwhavision-link] [Wisenet WAVE VMS][wisenetwave-link] (US licensed and OEM-branded version of Nx Witness) + +## Overview + +### Introduction + +I ran DW Spectrum in my home lab on an Ubuntu Virtual Machine, and was looking for a way to run it in Docker. At the time Network Optix provided no support for Docker, but I did find the [The Home Repot NxWitness][thehomegithub-link] project, that inspired me to create this project.\ +I started with individual repositories for Nx Witness, Nx Meta, and DW Spectrum, but that soon became cumbersome with lots of duplication, and I combined all product flavors into this one project. + +Today Network Optix supports [Docker][nxdocker-link], and they publish [build scripts][nxgithubdocker-link], but they do not publish container images. + +### Base Images + +The project creates two variants of each product using different base images: + +- [Ubuntu][ubuntu-link] using [ubuntu:noble][ubuntudocker-link] base image. +- [LinuxServer][lsio-link] using [lsiobase/ubuntu:noble][ubuntulsiodocker-link] base image. + +The build pipeline also publishes internal base images (`ptr727/nx-base:ubuntu-noble` and +`ptr727/nx-base-lsio:ubuntu-noble`) built from `ubuntu:noble` and `lsiobase/ubuntu:noble` +to reduce CI cache churn. These base images are not intended for end-user deployments. + +Note that smaller base images like [Alpine][alpine-link] are not [supported][nxossupport-link] by the mediaserver. + +### LinuxServer + +The [LinuxServer (LSIO)][lsio-link] base images provide valuable container functionality: + +- The LSIO images are based on [s6-overlay][s6overlay-link], are updated weekly, and LSIO [produces][lsiofleet-link] containers for many popular open source applications. +- LSIO allows us to [specify][lsiopuid-link] the user account to use when running the mediaserver, while still running the `root-tool` as `root` (required for license enforcement). +- Running as non-root is a [best practice][dockernonroot-link], and required if we need user specific permissions when accessing mapped volumes. +- The [nxvms-docker][nxgithubcompose-link] project takes a different approach running a compose stack that runs the mediaserver in one instance under the `${COMPANY_NAME}` account, and the root-tool in a second instance under the `root` account, using a shared `/tmp` volume for socket IPC between the mediaserver and root-tool, but the user account `${COMPANY_NAME}` does not readily map to a user on the host system. + +## Installation + +Images are published on [Docker Hub][hub-link]: + +- [NxWitness][nxwitness-docker-hub-link]: `docker pull docker.io/ptr727/nxwitness` +- [NxWitness-LSIO][nxwitness-lsio-docker-hub-link]: `docker pull docker.io/ptr727/nxwitness-lsio` +- [NxMeta][nxmeta-docker-hub-link]: `docker pull docker.io/ptr727/nxmeta` +- [NxMeta-LSIO][nxmeta-lsio-docker-hub-link]: `docker pull docker.io/ptr727/nxmeta-lsio` +- [NxGo][nxgo-docker-hub-link]: `docker pull docker.io/ptr727/nxgo` +- [NxGo-LSIO][nxgo-lsio-docker-hub-link]: `docker pull docker.io/ptr727/nxgo-lsio` +- [DWSpectrum][dwspectrum-docker-hub-link]: `docker pull docker.io/ptr727/dwspectrum` +- [DWSpectrum-LSIO][dwspectrum-lsio-docker-hub-link]: `docker pull docker.io/ptr727/dwspectrum-lsio` +- [WisenetWAVE][wisenetwave-docker-hub-link]: `docker pull docker.io/ptr727/wisenetwave` +- [WisenetWAVE-LSIO][wisenetwave-lsio-docker-hub-link]: `docker pull docker.io/ptr727/wisenetwave-lsio` + +Images are tagged as follows: + +- `latest`: Latest published version, e.g. `docker pull docker.io/ptr727/nxmeta:latest`. +- `stable`: Latest released version, e.g. `docker pull docker.io/ptr727/nxmeta:stable`. +- `rc`: Latest RC version, e.g. `docker pull docker.io/ptr727/nxmeta:rc`. +- `beta`: Latest Beta version, e.g. `docker pull docker.io/ptr727/nxmeta:beta` +- `develop`: Builds created from the develop branch, e.g. `docker pull docker.io/ptr727/nxmeta:develop`. +- `develop-stable`: The develop branch's stable build, e.g. `docker pull docker.io/ptr727/nxmeta:develop-stable`. +- `develop-[version]`: A develop build at a specific version, e.g. `docker pull docker.io/ptr727/nxmeta:develop-6.1.3.43301`. +- `[version]`: Release version number, e.g. `docker pull docker.io/ptr727/nxmeta:5.2.2.37996`. + +Notes: + +- `latest` and `stable` may be the same version if all builds are released builds. +- `rc` and `beta` tags are only built when Nx publishes RC and Beta builds. They may be older than the current `latest` or `stable` builds. +- One publish run covers one branch. The weekly schedule rebuilds `main` and picks up the latest upstream Ubuntu updates. +- A push to `main` changing `Make/Matrix.json` publishes at once, so a new Nx version ships without waiting for the schedule. Only a push by the codegen App or Dependabot triggers this. A push by a person does not publish. +- The `develop` tags are published by starting the publisher manually from `develop`. +- Merging code or dependency updates does not republish images. The published images change only when their content changes. +- See [Build Process][build-process] for more details. + +## Configuration + +User accounts and directory names are based on the product variant exposed by the `${COMPANY_NAME}` variable: + +- NxWitness: `networkoptix` +- DWSpectrum: `digitalwatchdog` +- NxMeta: `networkoptix-metavms` +- WisenetWAVE: `hanwha` + +### LSIO Volumes + +The LSIO images [re-link][init-nx-relocate] various internal paths to `/config`. + +- `/config` : Configuration files: + - `/opt/${COMPANY_NAME}/mediaserver/etc` links to `/config/etc` : Configuration. + - `/root/.config/nx_ini` links to `/config/ini` : Additional configuration. + - `/opt/${COMPANY_NAME}/mediaserver/var` links to `/config/var` : State and logs. +- `/media` : Recording files. + +### Non-LSIO Volumes + +The non-LSIO images must be mapped directly to the installed paths, refer to the [nxvms-docker][nxgithubvolumes-link] page for details. + +- `/opt/${COMPANY_NAME}/mediaserver/etc` : Configuration. +- `/home/${COMPANY_NAME}/.config/nx_ini` : Additional configuration. +- `/opt/${COMPANY_NAME}/mediaserver/var` : State and logs. +- `/media` : Recording files. + +### Ports + +- `7001` : Default server port. + +### Environment Variables + +- `PUID` : User Id, LSIO only, optional. +- `PGID` : Group Id, LSIO only, optional. +- `TZ` : Timezone, e.g. `America/Los_Angeles`. + +See [LSIO docs][lsiopuid-link] for usage of `PUID` and `PGID` that allow the mediaserver to run under a user account and the root-tool to run as root. + +### Network Mode + +Any network mode can be used, but due to the hardware bound licensing, `host` mode is [recommended][nxgithubnetworking-link]. + +## Examples + +### LSIO Docker Create + +```shell +docker create \ + --name=nxwitness-lsio-test-container \ + --hostname=nxwitness-lsio-test-host \ + --domainname=foo.bar.net \ + --restart=unless-stopped \ + --network=host \ + --env TZ=America/Los_Angeles \ + --volume /mnt/nxwitness/config:/config:rw \ + --volume /mnt/nxwitness/media:/media:rw \ + docker.io/ptr727/nxwitness-lsio:stable + +docker start nxwitness-lsio-test-container +``` + +### LSIO Docker Compose + +```yaml +services: + nxwitness: + image: docker.io/ptr727/nxwitness-lsio:stable + container_name: nxwitness-lsio-test-container + restart: unless-stopped + network_mode: host + environment: + # - PUID=65534 # id $user + # - PGID=65534 # id $group + - TZ=America/Los_Angeles + volumes: + - /mnt/nxwitness/config:/config + - /mnt/nxwitness/media:/media + tmpfs: + # Keep mediaserver's Unix socket and tmp files in RAM + - /tmp:size=1g,mode=1777 +``` + +### Non-LSIO Docker Compose + +```yaml +services: + nxwitness: + image: docker.io/ptr727/nxwitness:stable + container_name: nxwitness-test-container + restart: unless-stopped + network_mode: host + volumes: + - /mnt/nxwitness/config/etc:/opt/networkoptix/mediaserver/etc + - /mnt/nxwitness/config/nx_ini:/home/networkoptix/.config/nx_ini + - /mnt/nxwitness/config/var:/opt/networkoptix/mediaserver/var + - /mnt/nxwitness/media:/media + tmpfs: + # Keep mediaserver's Unix socket and tmp files in RAM + - /tmp:size=1g,mode=1777 +``` + +### Unraid Template + +- Add the template [URL][unraid] `https://github.com/ptr727/NxWitness/tree/main/Unraid` to the "Template Repositories" section, at the bottom of the "Docker" configuration tab, and click "Save". +- Create a new container by clicking the "Add Container" button, select the desired product template from the dropdown. +- If using Unassigned Devices for media storage, use `RW/Slave` access mode. +- Use `nobody` and `users` identifiers, `PUID=99` and `PGID=100`. +- Register the Unraid filesystems in the `additionalLocalFsTypes` advanced settings, see the [Missing Storage][missing-storage] section for help. + +## Product Information + +### Release Information + +- Nx Witness: + - [Releases JSON API][nxwitnessreleases-link] + - [Downloads][nxwitnessdownload-link] + - [Beta Downloads][nxwitnessbetadownload-link] + - [Release Notes][nxwitnessreleasenotes-link] +- Nx Meta: + - [Releases JSON API][nxmetareleases-link] + - [Signup for Nx Meta][getstartedwithmeta-link] + - [Request Developer Licenses][getalicense-link] + - [Downloads][nxmetadownload-link] + - [Beta Downloads][nxmetabetadownload-link] +- Nx Go: + - [Releases JSON API][nxgoreleases-link] + - [Downloads][nxgodownload-link] + - [Beta Downloads][nxgobetadownload-link] + - [Release Notes][nxgoreleasenotes-link] +- DW Spectrum: + - [Releases JSON API][dwspectrumreleases-link] + - [Downloads][dwspectrumdownload-link] + - [Release Notes][dwspectrumreleasenotes-link] +- Wisenet WAVE: + - [Releases JSON API][wisenetwavereleases-link] + - [Downloads][wisenetwavedownload-link] + - [Release Notes][wisenetwavereleasenotes-link] + +### Advanced Configuration + +- `mediaserver.conf` [Configuration][configoptions-link]: `https://[hostname]:[port]/#/server-documentation` +- `nx_vms_server.ini` [Configuration][iniconfig-link]: `https://[hostname]:[port]/api/iniConfig/` +- Advanced Server Configuration: `https://[hostname]:[port]/#/settings/advanced` +- Storage Reporting: `https://[hostname]:[port]/#/health/storages` + +## Build Process + +**Build overview**: + +- [`CreateMatrix`][create-matrix] is used to update available product versions, and to create Docker files for all product permutations. +- [`Version.json`][version-json] is updated using the mediaserver [Releases JSON API][nxwitnessreleases-link] and [Packages API][packages-link]. +- The logic follows the same pattern as used by the [Nx Open][releaseinfo-link] desktop client logic. +- The "released" status of a build follows the same method Nx uses in [`isBuildPublished()`][isbuildpublished-link]. From the [Releases JSON API][nxwitnessreleases-link], `release_date` must be greater than `0` and `release_delivery_days` must be `0` or greater. +- [`Matrix.json`][matrix-json] is created from the `Version.json` file and is used during pipeline builds using a [Matrix][matrix-link] strategy. +- Automated builds use [GitHub Actions][github-actions-docs-link]: + - Pull requests run unit tests, and a fast representative amd64 smoke build of `NxMeta` and `NxMeta-LSIO` ([`test-pull-request.yml`][test-pull-request-workflow]). The smoke build is gated on a change under `Docker/`, or to [`Matrix.json`][matrix-json] or [`Version.json`][version-json]. A version or matrix change is covered rather than skipped. The full matrix is not built on every PR. + - Publishing runs on one branch at a time ([`publish-release.yml`][publish-release-workflow]). The weekly schedule publishes `main`. A manual dispatch publishes whichever branch it is started from, which is how `develop` is published. + - A push to `main` that changes [`Matrix.json`][matrix-json] also publishes `main`, but only when the codegen App or Dependabot made it. A push by a person does not publish. + - Any other merge does not publish. A merge to `main` ships on the next weekly run, and a merge to `develop` ships only when someone dispatches the publisher from `develop`. +- Version history is maintained and used by `CreateMatrix` such that generic tags, e.g. `latest`, will never result in a lesser version number, i.e. break-fix-forward only, see [Issue #62][issue-62-link] for details on Nx re-publishing "released" builds using an older version breaking already upgraded systems. + +**Local testing**: + +- Run `cd ./Make` and [`./Test.sh`][test-sh], the following will be executed: + - [`Create.sh`][create-sh]: Creates the Dockerfiles and updates the latest version information using `CreateMatrix`. + - [`Build.sh`][build-sh]: Builds the Dockerfiles using `docker buildx build`. + - [`Up.sh`][up-sh]: Launch a Docker Compose stack [`Test.yml`][test-yml] to run all product variants. +- Ctrl-Click on the links to launch the web UI for each of the product variants. +- Run [`Clean.sh`][clean-sh] to shutdown the compose stack and cleanup images. + +## Known Issues + +- Licensing: + - Camera recording license keys are activated and bound to hardware attributes of the host server collected by the `root-tool` that is required to run as `root`. + - Requiring the `root-tool` to run as root overly complicates running the `mediaserver` as a non-root user, and requires the container to run using `host` networking to not break the hardware license checks. + - Docker containers are supposed to be portable, and moving containers between hosts will break license activation. + - Nx's own [`nxvms-docker`][nxgithubcompose-link] reference image [disabled `root-tool` in late 2025][nxvms-docker-roottool-link] by setting `ignoreRootTool=true` in `mediaserver.conf` and dropping their separate `root-tool` container. This trades hardware-ID license enforcement for a simpler unprivileged container. NxWitness does **not** follow this change, because licensed deployments would lose activation. This will be revisited only if Nx publishes a clearer official position on Docker licensing without `root-tool`. + - Nx to fix: Associate licenses with the [Cloud Account][nxcloud-link] not the local hardware. +- Storage Management: + - The mediaserver attempts to automatically decide what storage to use. + - Filesystem types are filtered out if not on the [supported list][nxgithubstorage-link]. + - Mounted volumes are ignored if backed by the same physical storage, even if logically separate. + - Unwanted `Nx MetaVMS Media` directories are created on any discoverable writable storage. + - Nx to fix: Eliminate the elaborate filesystem filter logic and use only the admin specified storage locations. +- Configuration Files: + - `.conf` configuration files are located in a static `mediaserver/etc` location while `.ini` configuration files are in a user-account dependent location, e.g. `/home/networkoptix/.config/nx_ini` or `/root/.config/nx_ini`. + - There is no value in having a server use per-user configuration directories, and it is inconsistent to mix configuration file locations. + - Nx to fix: Store all configuration files in `mediaserver/etc`. +- External Plugins: + - Custom or [Marketplace][nxmarketplace-link] plugins are installed in the `mediaserver/bin/plugins` directory. + - The `mediaserver/bin/plugins` directory is already pre-populated with Nx installed plugins. + - It is not possible to use external plugins from a mounted volume as the directory is already in-use. + - Nx to fix: Load plugins from `mediaserver/var/plugins` or from sub-directories mounted below `mediaserver/bin/plugins`, e.g. `mediaserver/bin/plugins/external` +- Lifetime Upgrades: + - Nx is a cloud product, free to view, free upgrades, comes with ongoing costs of hosting, maintenance, and support, it is [unfeasible][nxcrunchbase-link] to sustain a business with ongoing costs using perpetual one-off licenses. + - My personal experience with [Digital Watchdog][digitalwatchdog-link] and their [Lifetime Upgrades and No Annual Agreements][dwupgrades-link] is an inflexible policy of three activations per license and you have to buy a new license, thus the "license lifetime" is a multiplier of the "hardware lifetime". + - Nx to fix: Yearly camera license renewals covering the cost of support and upgrades. +- Archiving: + - Nx makes no distinction between recording and archiving storage, archive is basically just a recording mirror without any capacity or retention benefit. + - Recording storage is typically high speed low latency high cost low capacity SSD/NVMe arrays, while archival playback storage is very high capacity low cost magnetic media arrays. + - Nx to fix: Implement something akin to archiving in [Milestone XProtect VMS][milestone-link] where recording storage is separate from long term archival storage. +- Image Publication: + - Nx relies on end-users or projects like this one to create and publish Docker images. + - Nx to fix: Publish up-to-date images for all product variants and release channels. +- Break-Fix-Version-Forward: + - Nx product versions published via their releases API occasionally go backwards, e.g. `release`: v4.3 -> v5.0 -> v4.3. + - Nx supports forward-only in-place upgrades, e.g. v4.3 to v5.0, but not v5.0 to v4.3. + - Publishing generic tags, e.g. `latest`, using a version that regresses, e.g. v4.3 -> v5.0 -> v4.3 breaks deployments, see [Issue #62][issue-62-link] for details. + - `CreateMatrix` tooling keeps track of published versions, and prevents version regression of generic `latest`, `rc` and `beta` tags. + - Nx to fix: Release break-fix-version-forward only via release API's. + +## Troubleshooting + +Common deployment problems and how to diagnose them. + +### Missing Storage + +The following section will help troubleshoot common problems with missing storage.\ +If this does not help, please contact [Network Optix Support][nxsupport-link].\ +Please do not open a GitHub issue unless you are positive the issue is with the `Dockerfile`. + +Confirm that all the mounted volumes are listed in the available storage locations in the [web admin][nxwebadmin-link] portal. + +Enable [debug logging][nxdebuglogging-link] in the mediaserver:\ +Edit `mediaserver.conf`, set `logLevel=verbose`, restart the server.\ +Look for clues in `/config/var/log/log_file.log`. + +E.g. + +```log +VERBOSE nx::vms::server::fs: shfs /media fuse.shfs - duplicate +VERBOSE nx::vms::server::fs: /dev/sdb8 /media btrfs - duplicate +DEBUG QnStorageSpaceRestHandler(0x7f85043b0b00): Return 0 storages and 1 protocols +``` + +Get a list of the mapped volume mounts in the running container, and verify that `/config` and `/media` are listed in the `Mounts` section: + +```shell +docker ps --no-trunc +docker container inspect [containername] +``` + +Launch a shell in the running container and get a list of filesystems mounts: + +```shell +docker ps --no-trunc +docker exec --interactive --tty [containername] /bin/bash +cat /proc/mounts +exit +``` + +Example output for ZFS (note that ZFS support [was added][nxreleasenotes-link] in v5.0): + +```shell +ssdpool/appdata /config zfs rw,noatime,xattr,posixacl 0 0 +nvrpool/nvr /media zfs rw,noatime,xattr,posixacl 0 0 +ssdpool/docker /archive zfs rw,noatime,xattr,posixacl 0 0 +``` + +Mount `/config` is on device `ssdpool/appdata` and filesystem is `zfs`.\ +Mount `/media` is on device `nvrpool/nvr` and filesystem is `zfs`.\ +Mount `/archive` is on device `ssdpool/docker` and filesystem is `zfs`. + +In this case the devices are unique and will not be filtered, but `zfs` is not supported and needs to be registered. + +Example output for UnRaid FUSE: + +```shell +shfs /config fuse.shfs rw,nosuid,nodev,noatime,user_id=0,group_id=0,allow_other 0 0 +shfs /media fuse.shfs rw,nosuid,nodev,noatime,user_id=0,group_id=0,allow_other 0 0 +shfs /archive fuse.shfs rw,nosuid,nodev,noatime,user_id=0,group_id=0,allow_other 0 0 +``` + +In this case there are two issues, the device is `/shfs` for all three mounts and will be filtered, and the filesystem type is `fuse.shfs` that is not supported and needs to be registered. + +Log file output for Unraid FUSE: + +```log +VERBOSE nx::vms::server::fs: shfs /config fuse.shfs - added +VERBOSE nx::vms::server::fs: shfs /media fuse.shfs - added +VERBOSE nx::vms::server::fs: shfs /archive fuse.shfs - duplicate +``` + +The `/archive` mount is classified as a duplicate and ignored, map just `/media`, do not map `/archive`.\ +Alternative use the "Unassigned Devices" plugin and dedicate e.g. a XFS formatted SSD drive to `/media` and/or `/config`. + +Example output for Unraid BTRFS: + +```shell +/dev/sdb8 /test btrfs rw,relatime,space_cache,subvolid=5,subvol=/test 0 0 +/dev/sdb8 /config btrfs rw,relatime,space_cache,subvolid=5,subvol=/config 0 0 +/dev/sdb8 /media btrfs rw,relatime,space_cache,subvolid=5,subvol=/media 0 0 +/dev/sdb8 /archive btrfs rw,relatime,space_cache,subvolid=5,subvol=/archive 0 0 +``` + +```log +VERBOSE nx::vms::server::fs: /dev/sdb8 /test btrfs - added +VERBOSE nx::vms::server::fs: /dev/sdb8 /config btrfs - duplicate +VERBOSE nx::vms::server::fs: /dev/sdb8 /media btrfs - duplicate +VERBOSE nx::vms::server::fs: /dev/sdb8 /archive btrfs - duplicate +``` + +In this example the `/test` volume was accepted, but all other volumes on `/dev/sdb8` was ignored as duplicates. + +Add the required filesystem types in the [advanced configuration][advanced-configuration] menu. +Edit the `additionalLocalFsTypes` option and add the required filesystem types, e.g. `fuse.shfs,btrfs,zfs`, restart the server. + +Alternatively call the configuration API directly:\ +`wget --ask-password --user=[username] --ca-certificate=[server-cert.pem] https://[hostname]:[port]/api/systemSettings?additionalLocalFsTypes=fuse.shfs,btrfs,zfs` + +The mediaserver presents a self-signed certificate, which a client validating against the system trust store rejects. `--ca-certificate` trusts that one certificate rather than switching validation off, so export the server's certificate once and point at the file. `--ask-password` prompts instead of taking the password as an argument, which keeps it out of the shell history and the process list. Do not reach for `--no-check-certificate` here: it sends the credentials over a connection nobody has authenticated, which an active man-in-the-middle can capture. + +To my knowledge there is no solution to duplicate devices being filtered, please contact [Network Optix Support][nxsupport-link] and ask them to stop filtering filesystem types and devices. + +## Questions or Issues + +I am not affiliated with Network Optix, I cannot provide support for their products, please contact [Network Optix Support][nxsupport-link] for product support issues.\ +If there are issues with the Docker build scripts used in this project, please create a [GitHub Issue][issues-link].\ +Note that I only test and run `nxmeta-lsio:stable` in my home lab, other images get very little to no testing, please test accordingly. + +Read [Troubleshooting][troubleshooting] before filing an issue about missing or ignored storage, as the cause is almost always the mediaserver filesystem and device filtering described there. + +## 3rd Party Tools + +The third-party tools, libraries, and actions this project depends on. + +| Tool | Role | +| --- | --- | +| [actionlint][actionlint-link] | GitHub Actions workflow linter. | +| [AwesomeAssertions][awesomeassertions-link] | Assertion library for .NET tests. | +| [Codecov][codecov-link] | Code coverage reporting service. | +| [Create Pull Request][create-pull-request-link] | GitHub action that opens a pull request from workspace changes. | +| [CSharpier][csharpier-link] | C# code formatter. | +| [cspell][cspell-link] | Spell checker. | +| [Docker][docker-link] | Container build and runtime platform. | +| [Docker Hub Description][docker-hub-description-link] | GitHub action that publishes a Docker Hub repository overview. | +| [dotnet-outdated][dotnet-outdated-link] | Outdated NuGet dependency reporter. | +| [editorconfig-checker][editorconfig-checker-link] | Line-ending and whitespace linter. | +| [GH Release][gh-release-link] | GitHub action that creates a release. | +| [GitHub Actions][github-actions-link] | CI and automation runner. | +| [GitHub Dependabot][dependabot-link] | Dependency update bot. | +| [Husky.Net][husky-link] | Git hook manager for .NET. | +| [LinuxServer][lsio-link] | Publisher of container base images and application images. | +| [Markdown All in One][markdown-all-in-one-link] | Markdown editing extension. | +| [markdownlint-cli2][markdownlint-link] | Markdown linter. | +| [Nerdbank.GitVersioning][nbgv-link] | Version computation from git height. | +| [s6-overlay][s6overlay-link] | Process supervisor and init system for containers. | +| [Serilog][serilog-link] | Structured logging library for .NET. | +| [System.CommandLine][system-commandline-link] | Command line parsing library for .NET. | +| [Ubuntu][ubuntu-link] | Linux distribution. | +| [xUnit.Net][xunit-link] | Test framework for .NET. | + +## License + +Licensed under the [MIT License][license]\ +![GitHub License][license-shield] + +<!-- Sections --> + +[advanced-configuration]: #advanced-configuration +[build-process]: #build-process +[missing-storage]: #missing-storage +[troubleshooting]: #troubleshooting + +<!-- Shields --> + +[github-pre-release-shield]: https://img.shields.io/github/v/release/ptr727/NxWitness?include_prereleases&logo=github&label=GitHub%20Pre-Release +[github-release-shield]: https://img.shields.io/github/v/release/ptr727/NxWitness?logo=github&label=GitHub%20Release +[hubdwspectrumbeta-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum/beta?label=beta&logo=docker +[hubdwspectrumlatest-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum/latest?label=latest&logo=docker +[hubdwspectrumlsiobeta-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum-lsio/beta?label=beta&logo=docker +[hubdwspectrumlsiolatest-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum-lsio/latest?label=latest&logo=docker +[hubdwspectrumlsiorc-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum-lsio/rc?label=rc&logo=docker +[hubdwspectrumlsiostable-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum-lsio/stable?label=stable&logo=docker +[hubdwspectrumrc-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum/rc?label=rc&logo=docker +[hubdwspectrumstable-shield]: https://img.shields.io/docker/v/ptr727/dwspectrum/stable?label=stable&logo=docker +[hubnxgobeta-shield]: https://img.shields.io/docker/v/ptr727/nxgo/beta?label=beta&logo=docker +[hubnxgolatest-shield]: https://img.shields.io/docker/v/ptr727/nxgo/latest?label=latest&logo=docker +[hubnxgolsiobeta-shield]: https://img.shields.io/docker/v/ptr727/nxgo-lsio/beta?label=beta&logo=docker +[hubnxgolsiolatest-shield]: https://img.shields.io/docker/v/ptr727/nxgo-lsio/latest?label=latest&logo=docker +[hubnxgolsiorc-shield]: https://img.shields.io/docker/v/ptr727/nxgo-lsio/rc?label=rc&logo=docker +[hubnxgolsiostable-shield]: https://img.shields.io/docker/v/ptr727/nxgo-lsio/stable?label=stable&logo=docker +[hubnxgorc-shield]: https://img.shields.io/docker/v/ptr727/nxgo/rc?label=rc&logo=docker +[hubnxgostable-shield]: https://img.shields.io/docker/v/ptr727/nxgo/stable?label=stable&logo=docker +[hubnxmetabeta-shield]: https://img.shields.io/docker/v/ptr727/nxmeta/beta?label=beta&logo=docker +[hubnxmetalatest-shield]: https://img.shields.io/docker/v/ptr727/nxmeta/latest?label=latest&logo=docker +[hubnxmetalsiobeta-shield]: https://img.shields.io/docker/v/ptr727/nxmeta-lsio/beta?label=beta&logo=docker +[hubnxmetalsiolatest-shield]: https://img.shields.io/docker/v/ptr727/nxmeta-lsio/latest?label=latest&logo=docker +[hubnxmetalsiorc-shield]: https://img.shields.io/docker/v/ptr727/nxmeta-lsio/rc?label=rc&logo=docker +[hubnxmetalsiostable-shield]: https://img.shields.io/docker/v/ptr727/nxmeta-lsio/stable?label=stable&logo=docker +[hubnxmetarc-shield]: https://img.shields.io/docker/v/ptr727/nxmeta/rc?label=rc&logo=docker +[hubnxmetastable-shield]: https://img.shields.io/docker/v/ptr727/nxmeta/stable?label=stable&logo=docker +[hubnxwitnessbeta-shield]: https://img.shields.io/docker/v/ptr727/nxwitness/beta?label=beta&logo=docker +[hubnxwitnesslatest-shield]: https://img.shields.io/docker/v/ptr727/nxwitness/latest?label=latest&logo=docker +[hubnxwitnesslsiobeta-shield]: https://img.shields.io/docker/v/ptr727/nxwitness-lsio/beta?label=beta&logo=docker +[hubnxwitnesslsiolatest-shield]: https://img.shields.io/docker/v/ptr727/nxwitness-lsio/latest?label=latest&logo=docker +[hubnxwitnesslsiorc-shield]: https://img.shields.io/docker/v/ptr727/nxwitness-lsio/rc?label=rc&logo=docker +[hubnxwitnesslsiostable-shield]: https://img.shields.io/docker/v/ptr727/nxwitness-lsio/stable?label=stable&logo=docker +[hubnxwitnessrc-shield]: https://img.shields.io/docker/v/ptr727/nxwitness/rc?label=rc&logo=docker +[hubnxwitnessstable-shield]: https://img.shields.io/docker/v/ptr727/nxwitness/stable?label=stable&logo=docker +[hubwisenetwavebeta-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave/beta?label=beta&logo=docker +[hubwisenetwavelatest-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave/latest?label=latest&logo=docker +[hubwisenetwavelsiobeta-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave-lsio/beta?label=beta&logo=docker +[hubwisenetwavelsiolatest-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave-lsio/latest?label=latest&logo=docker +[hubwisenetwavelsiorc-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave-lsio/rc?label=rc&logo=docker +[hubwisenetwavelsiostable-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave-lsio/stable?label=stable&logo=docker +[hubwisenetwaverc-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave/rc?label=rc&logo=docker +[hubwisenetwavestable-shield]: https://img.shields.io/docker/v/ptr727/wisenetwave/stable?label=stable&logo=docker +[lastcommit-shield]: https://img.shields.io/github/last-commit/ptr727/NxWitness?logo=github&label=Last%20Commit +[license-shield]: https://img.shields.io/github/license/ptr727/NxWitness +[releasebuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/NxWitness/publish-release.yml?branch=main&logo=github&label=Build%20Status + +<!-- Distribution --> + +[actions-link]: https://github.com/ptr727/NxWitness/actions +[dwspectrum-docker-hub-link]: https://hub.docker.com/r/ptr727/dwspectrum +[dwspectrum-lsio-docker-hub-link]: https://hub.docker.com/r/ptr727/dwspectrum-lsio +[github-link]: https://github.com/ptr727/NxWitness +[hub-link]: https://hub.docker.com/u/ptr727 +[issue-62-link]: https://github.com/ptr727/NxWitness/issues/62 +[issues-link]: https://github.com/ptr727/NxWitness/issues +[nxgo-docker-hub-link]: https://hub.docker.com/r/ptr727/nxgo +[nxgo-lsio-docker-hub-link]: https://hub.docker.com/r/ptr727/nxgo-lsio +[nxmeta-docker-hub-link]: https://hub.docker.com/r/ptr727/nxmeta +[nxmeta-lsio-docker-hub-link]: https://hub.docker.com/r/ptr727/nxmeta-lsio +[nxwitness-docker-hub-link]: https://hub.docker.com/r/ptr727/nxwitness +[nxwitness-lsio-docker-hub-link]: https://hub.docker.com/r/ptr727/nxwitness-lsio +[releases-link]: https://github.com/ptr727/NxWitness/releases +[wisenetwave-docker-hub-link]: https://hub.docker.com/r/ptr727/wisenetwave +[wisenetwave-lsio-docker-hub-link]: https://hub.docker.com/r/ptr727/wisenetwave-lsio + +<!-- Repo --> + +[build-sh]: ./Make/Build.sh +[clean-sh]: ./Make/Clean.sh +[create-matrix]: ./CreateMatrix/ +[create-sh]: ./Make/Create.sh +[history]: ./HISTORY.md +[init-nx-relocate]: ./Docker/s6-overlay/s6-rc.d/init-nx-relocate/run +[license]: ./LICENSE +[matrix-json]: ./Make/Matrix.json +[publish-release-workflow]: ./.github/workflows/publish-release.yml +[test-pull-request-workflow]: ./.github/workflows/test-pull-request.yml +[test-sh]: ./Make/Test.sh +[test-yml]: ./Make/Test.yml +[unraid]: ./Unraid +[up-sh]: ./Make/Up.sh +[version-json]: ./Make/Version.json + +<!-- External --> + +[actionlint-link]: https://github.com/rhysd/actionlint +[alpine-link]: https://alpinelinux.org/ +[awesomeassertions-link]: https://awesomeassertions.org/ +[codecov-link]: https://about.codecov.io/ +[configoptions-link]: https://support.networkoptix.com/hc/en-us/articles/360036389693-How-to-access-Nx-Server-configuration-options +[create-pull-request-link]: https://github.com/marketplace/actions/create-pull-request +[csharpier-link]: https://csharpier.com/ +[cspell-link]: https://cspell.org +[dependabot-link]: https://github.com/dependabot +[digitalwatchdog-link]: https://digital-watchdog.com/ +[docker-hub-description-link]: https://github.com/marketplace/actions/docker-hub-description +[docker-link]: https://www.docker.com/ +[dockernonroot-link]: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user +[dotnet-outdated-link]: https://github.com/dotnet-outdated/dotnet-outdated +[dwspectrum-link]: https://dwspectrum.com/ +[dwspectrumdownload-link]: https://dwspectrum.digital-watchdog.com/download/linux +[dwspectrumreleasenotes-link]: https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html +[dwspectrumreleases-link]: https://updates.vmsproxy.com/digitalwatchdog/releases.json +[dwupgrades-link]: https://dwspectrum.com/upgrades/ +[editorconfig-checker-link]: https://github.com/editorconfig-checker/editorconfig-checker +[getalicense-link]: https://support.networkoptix.com/hc/en-us/articles/8693698259607-Get-a-License-for-Developers +[getstartedwithmeta-link]: https://www.networkoptix.com/nx-meta/get-started-with-meta +[gh-release-link]: https://github.com/marketplace/actions/gh-release +[github-actions-docs-link]: https://docs.github.com/en/actions +[github-actions-link]: https://github.com/actions +[hanwhavision-link]: https://hanwhavisionamerica.com/ +[husky-link]: https://alirezanet.github.io/Husky.Net/ +[iniconfig-link]: https://meta.nxvms.com/docs/developers/knowledgebase/241-configuring-via-ini-files--iniconfig +[isbuildpublished-link]: https://github.com/networkoptix/nx_open/blob/526967920636d3119c92a5220290ecc10957bf12/vms/libs/nx_vms_update/src/nx/vms/update/releases_info.cpp#L31 +[lsio-link]: https://www.linuxserver.io/ +[lsiofleet-link]: https://fleet.linuxserver.io/ +[lsiopuid-link]: https://docs.linuxserver.io/general/understanding-puid-and-pgid +[markdown-all-in-one-link]: https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one +[markdownlint-link]: https://github.com/DavidAnson/markdownlint-cli2 +[matrix-link]: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs +[milestone-link]: https://doc.milestonesys.com/latest/en-US/standard_features/sf_mc/sf_systemoverview/mc_storageandarchivingexplained.htm +[nbgv-link]: https://github.com/dotnet/Nerdbank.GitVersioning +[networkoptix-link]: https://www.networkoptix.com/ +[nxcloud-link]: https://www.networkoptix.com/nx-witness/nx-witness-cloud/ +[nxcrunchbase-link]: https://www.crunchbase.com/organization/network-optix +[nxdebuglogging-link]: https://support.networkoptix.com/hc/en-us/articles/236033688-How-to-change-software-logging-level-and-how-to-get-logs +[nxdocker-link]: https://support.networkoptix.com/hc/en-us/articles/360037973573-Docker +[nxgithubcompose-link]: https://github.com/networkoptix/nxvms-docker/blob/master/docker-compose.yaml +[nxgithubdocker-link]: https://github.com/networkoptix/nxvms-docker +[nxgithubnetworking-link]: https://github.com/networkoptix/nxvms-docker#networking +[nxgithubstorage-link]: https://github.com/networkoptix/nxvms-docker#notes-about-storage +[nxgithubvolumes-link]: https://github.com/networkoptix/nxvms-docker#volumes-description +[nxgo-link]: https://updates.networkoptix.com/nxgo +[nxgobetadownload-link]: https://cloud.nxgo.io/download/betas/linux +[nxgodownload-link]: https://cloud.nxgo.io/download/releases/linux +[nxgoreleasenotes-link]: https://updates.networkoptix.com/nxgo/#releases_list +[nxgoreleases-link]: https://updates.networkoptix.com/nxgo/releases.json +[nxmarketplace-link]: https://www.networkoptix.com/nx-meta/nx-integrations-marketplace/ +[nxmeta-link]: https://meta.nxvms.com/ +[nxmetabetadownload-link]: https://meta.nxvms.com/downloads/betas +[nxmetadownload-link]: https://meta.nxvms.com/download/linux +[nxmetareleases-link]: https://updates.vmsproxy.com/metavms/releases.json +[nxossupport-link]: https://support.networkoptix.com/hc/en-us/articles/205313168-Nx-Witness-Operating-System-Support +[nxreleasenotes-link]: https://support.networkoptix.com/hc/en-us/articles/360042751193-Current-and-Past-Releases-Downloads-Release-Notes +[nxsupport-link]: https://support.networkoptix.com/hc/en-us/community/topics +[nxvms-docker-roottool-link]: https://github.com/networkoptix/nxvms-docker/commit/4285f93 +[nxwebadmin-link]: https://support.networkoptix.com/hc/en-us/articles/115012831028-Nx-Server-Web-Admin +[nxwitness-link]: https://www.networkoptix.com/nx-witness/ +[nxwitnessbetadownload-link]: https://beta.networkoptix.com/beta-builds/default +[nxwitnessdownload-link]: https://nxvms.com/download/linux +[nxwitnessreleasenotes-link]: https://www.networkoptix.com/all-nx-witness-release-notes +[nxwitnessreleases-link]: https://updates.vmsproxy.com/default/releases.json +[packages-link]: https://updates.networkoptix.com/default/38363/packages.json +[releaseinfo-link]: https://github.com/networkoptix/nx_open/blob/master/vms/libs/nx_vms_update/src/nx/vms/update/releases_info.cpp +[s6overlay-link]: https://github.com/just-containers/s6-overlay +[serilog-link]: https://serilog.net/ +[system-commandline-link]: https://learn.microsoft.com/en-us/dotnet/standard/commandline/ +[thehomegithub-link]: https://github.com/thehomerepot/nxwitness +[ubuntu-link]: https://ubuntu.com/ +[ubuntudocker-link]: https://hub.docker.com/_/ubuntu +[ubuntulsiodocker-link]: https://hub.docker.com/r/lsiobase/ubuntu +[wisenetwave-link]: https://wavevms.com/ +[wisenetwavedownload-link]: https://wavevms.com/download/linux +[wisenetwavereleasenotes-link]: https://wavevms.com/release-notes/ +[wisenetwavereleases-link]: https://updates.vmsproxy.com/hanwha/releases.json +[xunit-link]: https://xunit.net/ diff --git a/Samples/Downloads/DWSpectrum.1.json b/Samples/Downloads/DWSpectrum.1.json index 2fbeb151..5daac522 100644 --- a/Samples/Downloads/DWSpectrum.1.json +++ b/Samples/Downloads/DWSpectrum.1.json @@ -1,350 +1,350 @@ -{ - "version": "5.0.0.35271", - "releaseNotes": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", - "product": "dwspectrum", - "productDescription": "Video Management System", - "date": "26 Jul 2022", - "buildNumber": "35271", - "password": "lqdo89", - "type": "release", - "installers": [ - { - "platform": "linux_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-linux_x64.deb", - "path": "linux/dwspectrum-client-5.0.0.35271-linux_x64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-linux_x64.deb", - "path": "linux/dwspectrum-server-5.0.0.35271-linux_x64.deb", - "niceName": "Ubuntu x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-bundle-5.0.0.35271-windows_x64.exe", - "path": "windows/dwspectrum-bundle-5.0.0.35271-windows_x64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "windows_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-windows_x64.exe", - "path": "windows/dwspectrum-client-5.0.0.35271-windows_x64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-5.0.0.35271-Net2v5.exe", - "path": "windows/dwspectrum-paxton_plugin-5.0.0.35271-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-5.0.0.35271-Net2v6.exe", - "path": "windows/dwspectrum-paxton_plugin-5.0.0.35271-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "windows_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-windows_x64.exe", - "path": "windows/dwspectrum-server-5.0.0.35271-windows_x64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-testcamera-5.0.0.35271-windows_x64.zip", - "path": "windows/dwspectrum-testcamera-5.0.0.35271-windows_x64.zip", - "niceName": "Windows x64 - Test camera" - }, - { - "platform": "macos_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-macos_x64.dmg", - "path": "macos/dwspectrum-client-5.0.0.35271-macos_x64.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "macos_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-macos_arm64.dmg", - "path": "macos/dwspectrum-client-5.0.0.35271-macos_arm64.dmg", - "niceName": "Mac OS ARM64 - Client installer" - }, - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-edge1.zip", - "path": "arm/dwspectrum-server-5.0.0.35271-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-linux_arm64.deb", - "path": "arm/dwspectrum-server-5.0.0.35271-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-linux_arm64.deb", - "path": "arm/dwspectrum-client-5.0.0.35271-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-linux_arm32.deb", - "path": "arm/dwspectrum-server-5.0.0.35271-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-metadata_sdk-5.0.0.35271-universal.zip", - "path": "sdk/dwspectrum-metadata_sdk-5.0.0.35271-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-storage_sdk-5.0.0.35271-universal.zip", - "path": "sdk/dwspectrum-storage_sdk-5.0.0.35271-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - }, - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-video_source_sdk-5.0.0.35271-universal.zip", - "path": "sdk/dwspectrum-video_source_sdk-5.0.0.35271-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - } - ], - "platforms": [ - { - "name": "linux", - "files": [ - { - "platform": "linux_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-linux_x64.deb", - "path": "linux/dwspectrum-client-5.0.0.35271-linux_x64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-linux_x64.deb", - "path": "linux/dwspectrum-server-5.0.0.35271-linux_x64.deb", - "niceName": "Ubuntu x64 - Server installer" - } - ] - }, - { - "name": "windows", - "files": [ - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-5.0.0.35271-Net2v5.exe", - "path": "windows/dwspectrum-paxton_plugin-5.0.0.35271-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-5.0.0.35271-Net2v6.exe", - "path": "windows/dwspectrum-paxton_plugin-5.0.0.35271-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "windows_x64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-bundle-5.0.0.35271-windows_x64.exe", - "path": "windows/dwspectrum-bundle-5.0.0.35271-windows_x64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "windows_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-windows_x64.exe", - "path": "windows/dwspectrum-client-5.0.0.35271-windows_x64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "windows_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-windows_x64.exe", - "path": "windows/dwspectrum-server-5.0.0.35271-windows_x64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-testcamera-5.0.0.35271-windows_x64.zip", - "path": "windows/dwspectrum-testcamera-5.0.0.35271-windows_x64.zip", - "niceName": "Windows x64 - Test camera" - } - ] - }, - { - "name": "macos", - "files": [ - { - "platform": "macos_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-macos_x64.dmg", - "path": "macos/dwspectrum-client-5.0.0.35271-macos_x64.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "macos_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-macos_arm64.dmg", - "path": "macos/dwspectrum-client-5.0.0.35271-macos_arm64.dmg", - "niceName": "Mac OS ARM64 - Client installer" - } - ] - }, - { - "name": "arm", - "files": [ - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-edge1.zip", - "path": "arm/dwspectrum-server-5.0.0.35271-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-5.0.0.35271-linux_arm64.deb", - "path": "arm/dwspectrum-client-5.0.0.35271-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-linux_arm64.deb", - "path": "arm/dwspectrum-server-5.0.0.35271-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-5.0.0.35271-linux_arm32.deb", - "path": "arm/dwspectrum-server-5.0.0.35271-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - } - ] - }, - { - "name": "sdk", - "files": [ - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-video_source_sdk-5.0.0.35271-universal.zip", - "path": "sdk/dwspectrum-video_source_sdk-5.0.0.35271-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-metadata_sdk-5.0.0.35271-universal.zip", - "path": "sdk/dwspectrum-metadata_sdk-5.0.0.35271-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-storage_sdk-5.0.0.35271-universal.zip", - "path": "sdk/dwspectrum-storage_sdk-5.0.0.35271-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - } - ] - } - ], - "backwardsCompatible": true, - "cloudGroup": "prod", - "beta": false, - "dismissed": false, - "releaseUrl": "https://updates.networkoptix.com/digitalwatchdog/35271/" +{ + "version": "5.0.0.35271", + "releaseNotes": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", + "product": "dwspectrum", + "productDescription": "Video Management System", + "date": "26 Jul 2022", + "buildNumber": "35271", + "password": "lqdo89", + "type": "release", + "installers": [ + { + "platform": "linux_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-linux_x64.deb", + "path": "linux/dwspectrum-client-5.0.0.35271-linux_x64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-linux_x64.deb", + "path": "linux/dwspectrum-server-5.0.0.35271-linux_x64.deb", + "niceName": "Ubuntu x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-bundle-5.0.0.35271-windows_x64.exe", + "path": "windows/dwspectrum-bundle-5.0.0.35271-windows_x64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "windows_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-windows_x64.exe", + "path": "windows/dwspectrum-client-5.0.0.35271-windows_x64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-5.0.0.35271-Net2v5.exe", + "path": "windows/dwspectrum-paxton_plugin-5.0.0.35271-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-5.0.0.35271-Net2v6.exe", + "path": "windows/dwspectrum-paxton_plugin-5.0.0.35271-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "windows_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-windows_x64.exe", + "path": "windows/dwspectrum-server-5.0.0.35271-windows_x64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-testcamera-5.0.0.35271-windows_x64.zip", + "path": "windows/dwspectrum-testcamera-5.0.0.35271-windows_x64.zip", + "niceName": "Windows x64 - Test camera" + }, + { + "platform": "macos_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-macos_x64.dmg", + "path": "macos/dwspectrum-client-5.0.0.35271-macos_x64.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "macos_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-macos_arm64.dmg", + "path": "macos/dwspectrum-client-5.0.0.35271-macos_arm64.dmg", + "niceName": "Mac OS ARM64 - Client installer" + }, + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-edge1.zip", + "path": "arm/dwspectrum-server-5.0.0.35271-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-linux_arm64.deb", + "path": "arm/dwspectrum-server-5.0.0.35271-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-linux_arm64.deb", + "path": "arm/dwspectrum-client-5.0.0.35271-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-linux_arm32.deb", + "path": "arm/dwspectrum-server-5.0.0.35271-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-metadata_sdk-5.0.0.35271-universal.zip", + "path": "sdk/dwspectrum-metadata_sdk-5.0.0.35271-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-storage_sdk-5.0.0.35271-universal.zip", + "path": "sdk/dwspectrum-storage_sdk-5.0.0.35271-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + }, + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-video_source_sdk-5.0.0.35271-universal.zip", + "path": "sdk/dwspectrum-video_source_sdk-5.0.0.35271-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + } + ], + "platforms": [ + { + "name": "linux", + "files": [ + { + "platform": "linux_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-linux_x64.deb", + "path": "linux/dwspectrum-client-5.0.0.35271-linux_x64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-linux_x64.deb", + "path": "linux/dwspectrum-server-5.0.0.35271-linux_x64.deb", + "niceName": "Ubuntu x64 - Server installer" + } + ] + }, + { + "name": "windows", + "files": [ + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-5.0.0.35271-Net2v5.exe", + "path": "windows/dwspectrum-paxton_plugin-5.0.0.35271-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-5.0.0.35271-Net2v6.exe", + "path": "windows/dwspectrum-paxton_plugin-5.0.0.35271-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "windows_x64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-bundle-5.0.0.35271-windows_x64.exe", + "path": "windows/dwspectrum-bundle-5.0.0.35271-windows_x64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "windows_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-windows_x64.exe", + "path": "windows/dwspectrum-client-5.0.0.35271-windows_x64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "windows_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-windows_x64.exe", + "path": "windows/dwspectrum-server-5.0.0.35271-windows_x64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-testcamera-5.0.0.35271-windows_x64.zip", + "path": "windows/dwspectrum-testcamera-5.0.0.35271-windows_x64.zip", + "niceName": "Windows x64 - Test camera" + } + ] + }, + { + "name": "macos", + "files": [ + { + "platform": "macos_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-macos_x64.dmg", + "path": "macos/dwspectrum-client-5.0.0.35271-macos_x64.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "macos_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-macos_arm64.dmg", + "path": "macos/dwspectrum-client-5.0.0.35271-macos_arm64.dmg", + "niceName": "Mac OS ARM64 - Client installer" + } + ] + }, + { + "name": "arm", + "files": [ + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-edge1.zip", + "path": "arm/dwspectrum-server-5.0.0.35271-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-5.0.0.35271-linux_arm64.deb", + "path": "arm/dwspectrum-client-5.0.0.35271-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-linux_arm64.deb", + "path": "arm/dwspectrum-server-5.0.0.35271-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-5.0.0.35271-linux_arm32.deb", + "path": "arm/dwspectrum-server-5.0.0.35271-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + } + ] + }, + { + "name": "sdk", + "files": [ + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-video_source_sdk-5.0.0.35271-universal.zip", + "path": "sdk/dwspectrum-video_source_sdk-5.0.0.35271-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-metadata_sdk-5.0.0.35271-universal.zip", + "path": "sdk/dwspectrum-metadata_sdk-5.0.0.35271-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-storage_sdk-5.0.0.35271-universal.zip", + "path": "sdk/dwspectrum-storage_sdk-5.0.0.35271-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + } + ] + } + ], + "backwardsCompatible": true, + "cloudGroup": "prod", + "beta": false, + "dismissed": false, + "releaseUrl": "https://updates.networkoptix.com/digitalwatchdog/35271/" } \ No newline at end of file diff --git a/Samples/Downloads/DWSpectrum.2.json b/Samples/Downloads/DWSpectrum.2.json index 47e0e1e0..597cb744 100644 --- a/Samples/Downloads/DWSpectrum.2.json +++ b/Samples/Downloads/DWSpectrum.2.json @@ -1,332 +1,332 @@ -{ - "version": "4.2.0.32842", - "releaseNotes": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", - "product": "dwspectrum", - "productDescription": "Video Management System", - "date": "27 Apr 2021", - "buildNumber": "32842", - "password": "hkh1td", - "type": "release", - "installers": [ - { - "platform": "linux64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Server installer" - }, - { - "platform": "win64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "win64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "win64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "win64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", - "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", - "niceName": "Windows x64 - Test camera" - }, - { - "platform": "mac", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", - "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", - "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - }, - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - } - ], - "platforms": [ - { - "name": "linux", - "files": [ - { - "platform": "linux64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Server installer" - } - ] - }, - { - "name": "windows", - "files": [ - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "win64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "win64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "win64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "win64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", - "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", - "niceName": "Windows x64 - Test camera" - } - ] - }, - { - "name": "macos", - "files": [ - { - "platform": "mac", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", - "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", - "niceName": "Mac OS - Client installer" - } - ] - }, - { - "name": "arm", - "files": [ - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", - "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - } - ] - }, - { - "name": "sdk", - "files": [ - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - } - ] - } - ], - "backwardsCompatible": true, - "cloudGroup": "prod", - "beta": false, - "dismissed": false, - "releaseUrl": "https://updates.networkoptix.com/digitalwatchdog/32842/" +{ + "version": "4.2.0.32842", + "releaseNotes": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", + "product": "dwspectrum", + "productDescription": "Video Management System", + "date": "27 Apr 2021", + "buildNumber": "32842", + "password": "hkh1td", + "type": "release", + "installers": [ + { + "platform": "linux64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Server installer" + }, + { + "platform": "win64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "win64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "win64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "win64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", + "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", + "niceName": "Windows x64 - Test camera" + }, + { + "platform": "mac", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", + "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", + "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + }, + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + } + ], + "platforms": [ + { + "name": "linux", + "files": [ + { + "platform": "linux64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Server installer" + } + ] + }, + { + "name": "windows", + "files": [ + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "win64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "win64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "win64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "win64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", + "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", + "niceName": "Windows x64 - Test camera" + } + ] + }, + { + "name": "macos", + "files": [ + { + "platform": "mac", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", + "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", + "niceName": "Mac OS - Client installer" + } + ] + }, + { + "name": "arm", + "files": [ + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", + "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + } + ] + }, + { + "name": "sdk", + "files": [ + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + } + ] + } + ], + "backwardsCompatible": true, + "cloudGroup": "prod", + "beta": false, + "dismissed": false, + "releaseUrl": "https://updates.networkoptix.com/digitalwatchdog/32842/" } \ No newline at end of file diff --git a/Samples/Downloads/DWSpectrum.3.json b/Samples/Downloads/DWSpectrum.3.json index 47e0e1e0..597cb744 100644 --- a/Samples/Downloads/DWSpectrum.3.json +++ b/Samples/Downloads/DWSpectrum.3.json @@ -1,332 +1,332 @@ -{ - "version": "4.2.0.32842", - "releaseNotes": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", - "product": "dwspectrum", - "productDescription": "Video Management System", - "date": "27 Apr 2021", - "buildNumber": "32842", - "password": "hkh1td", - "type": "release", - "installers": [ - { - "platform": "linux64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Server installer" - }, - { - "platform": "win64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "win64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "win64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "win64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", - "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", - "niceName": "Windows x64 - Test camera" - }, - { - "platform": "mac", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", - "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", - "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - }, - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - } - ], - "platforms": [ - { - "name": "linux", - "files": [ - { - "platform": "linux64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Server installer" - } - ] - }, - { - "name": "windows", - "files": [ - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "win64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "win64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "win64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "win64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", - "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", - "niceName": "Windows x64 - Test camera" - } - ] - }, - { - "name": "macos", - "files": [ - { - "platform": "mac", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", - "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", - "niceName": "Mac OS - Client installer" - } - ] - }, - { - "name": "arm", - "files": [ - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", - "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - } - ] - }, - { - "name": "sdk", - "files": [ - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - } - ] - } - ], - "backwardsCompatible": true, - "cloudGroup": "prod", - "beta": false, - "dismissed": false, - "releaseUrl": "https://updates.networkoptix.com/digitalwatchdog/32842/" +{ + "version": "4.2.0.32842", + "releaseNotes": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", + "product": "dwspectrum", + "productDescription": "Video Management System", + "date": "27 Apr 2021", + "buildNumber": "32842", + "password": "hkh1td", + "type": "release", + "installers": [ + { + "platform": "linux64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Server installer" + }, + { + "platform": "win64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "win64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "win64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "win64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", + "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", + "niceName": "Windows x64 - Test camera" + }, + { + "platform": "mac", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", + "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", + "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + }, + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + } + ], + "platforms": [ + { + "name": "linux", + "files": [ + { + "platform": "linux64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Server installer" + } + ] + }, + { + "name": "windows", + "files": [ + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "win64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "win64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "win64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "win64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", + "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", + "niceName": "Windows x64 - Test camera" + } + ] + }, + { + "name": "macos", + "files": [ + { + "platform": "mac", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", + "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", + "niceName": "Mac OS - Client installer" + } + ] + }, + { + "name": "arm", + "files": [ + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", + "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + } + ] + }, + { + "name": "sdk", + "files": [ + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + } + ] + } + ], + "backwardsCompatible": true, + "cloudGroup": "prod", + "beta": false, + "dismissed": false, + "releaseUrl": "https://updates.networkoptix.com/digitalwatchdog/32842/" } \ No newline at end of file diff --git a/Samples/Downloads/DWspectrum.json b/Samples/Downloads/DWspectrum.json index 9b61b3bb..061c9e3f 100644 --- a/Samples/Downloads/DWspectrum.json +++ b/Samples/Downloads/DWspectrum.json @@ -1,291 +1,291 @@ -{ - "version": "4.2.0.32842", - "releaseNotes": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", - "product": "dwspectrum", - "productDescription": "Video Management System", - "date": "27 Apr 2021", - "buildNumber": "32842", - "password": "hkh1td", - "type": "release", - "installers": [ - { - "platform": "linux64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Server installer" - }, - { - "platform": "win64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "win64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "win64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "win64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", - "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", - "niceName": "Windows x64 - Test camera" - }, - { - "platform": "mac", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", - "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", - "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - }, - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - } - ], - "platforms": [ - { - "name": "linux", - "files": [ - { - "platform": "linux64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", - "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", - "niceName": "Ubuntu x64 - Server installer" - } - ] - }, - { - "name": "windows", - "files": [ - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "win64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "win64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "win64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", - "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "win64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", - "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", - "niceName": "Windows x64 - Test camera" - } - ] - }, - { - "name": "macos", - "files": [ - { - "platform": "mac", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", - "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", - "niceName": "Mac OS - Client installer" - } - ] - }, - { - "name": "sdk", - "files": [ - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - } - ] - } - ], - "backwardsCompatible": true, - "cloudGroup": "prod", - "beta": false, - "dismissed": false, - "releaseUrl": "https://updates.networkoptix.com/digitalwatchdog/32842/" +{ + "version": "4.2.0.32842", + "releaseNotes": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", + "product": "dwspectrum", + "productDescription": "Video Management System", + "date": "27 Apr 2021", + "buildNumber": "32842", + "password": "hkh1td", + "type": "release", + "installers": [ + { + "platform": "linux64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Server installer" + }, + { + "platform": "win64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "win64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "win64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "win64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", + "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", + "niceName": "Windows x64 - Test camera" + }, + { + "platform": "mac", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", + "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-edge1.zip", + "path": "arm/dwspectrum-server-4.2.0.32842-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-client-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "path": "arm/dwspectrum-server-4.2.0.32842-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + }, + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + } + ], + "platforms": [ + { + "name": "linux", + "files": [ + { + "platform": "linux64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-client-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-linux64.deb", + "path": "linux/dwspectrum-server-4.2.0.32842-linux64.deb", + "niceName": "Ubuntu x64 - Server installer" + } + ] + }, + { + "name": "windows", + "files": [ + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "path": "windows/dwspectrum-paxton_plugin-4.2.0.32842-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "win64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-bundle-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-bundle-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "win64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-client-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "win64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-server-4.2.0.32842-win64.exe", + "path": "windows/dwspectrum-server-4.2.0.32842-win64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "win64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-testcamera-4.2.0.32842-win64.zip", + "path": "windows/dwspectrum-testcamera-4.2.0.32842-win64.zip", + "niceName": "Windows x64 - Test camera" + } + ] + }, + { + "name": "macos", + "files": [ + { + "platform": "mac", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-client-4.2.0.32842-mac.dmg", + "path": "macos/dwspectrum-client-4.2.0.32842-mac.dmg", + "niceName": "Mac OS - Client installer" + } + ] + }, + { + "name": "sdk", + "files": [ + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-video_source_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-metadata_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "path": "sdk/dwspectrum-storage_sdk-4.2.0.32842-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + } + ] + } + ], + "backwardsCompatible": true, + "cloudGroup": "prod", + "beta": false, + "dismissed": false, + "releaseUrl": "https://updates.networkoptix.com/digitalwatchdog/32842/" } \ No newline at end of file diff --git a/Samples/Downloads/NxMeta.json b/Samples/Downloads/NxMeta.json index a5d142df..62f208bd 100644 --- a/Samples/Downloads/NxMeta.json +++ b/Samples/Downloads/NxMeta.json @@ -1,332 +1,332 @@ -{ - "version": "5.0.0.35269 R11", - "releaseNotes": "https://meta.nxvms.com/", - "product": "metavms", - "productDescription": "Video Management System", - "date": "26 Jul 2022", - "buildNumber": "35269", - "password": "pvj899", - "type": "release", - "installers": [ - { - "platform": "linux_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-linux_x64.deb", - "path": "linux/metavms-client-5.0.0.35269-linux_x64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-server-5.0.0.35269-linux_x64.deb", - "path": "linux/metavms-server-5.0.0.35269-linux_x64.deb", - "niceName": "Ubuntu x64 - Server installer" - }, - { - "platform": "linux_x64", - "appType": "vms_benchmark", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-vms_benchmark-5.0.0.35269-linux_x64.zip", - "path": "linux/metavms-vms_benchmark-5.0.0.35269-linux_x64.zip", - "niceName": "Ubuntu x64 - VMS Benchmark Tool" - }, - { - "platform": "windows_x64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-bundle-5.0.0.35269-windows_x64.exe", - "path": "windows/metavms-bundle-5.0.0.35269-windows_x64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "windows_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-windows_x64.exe", - "path": "windows/metavms-client-5.0.0.35269-windows_x64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "windows_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-server-5.0.0.35269-windows_x64.exe", - "path": "windows/metavms-server-5.0.0.35269-windows_x64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-testcamera-5.0.0.35269-windows_x64.zip", - "path": "windows/metavms-testcamera-5.0.0.35269-windows_x64.zip", - "niceName": "Windows x64 - Test camera" - }, - { - "platform": "windows_x64", - "appType": "vms_benchmark", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-vms_benchmark-5.0.0.35269-windows_x64.zip", - "path": "windows/metavms-vms_benchmark-5.0.0.35269-windows_x64.zip", - "niceName": "Windows x64 - VMS Benchmark Tool" - }, - { - "platform": "macos_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-macos_x64.dmg", - "path": "macos/metavms-client-5.0.0.35269-macos_x64.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "macos_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-macos_arm64.dmg", - "path": "macos/metavms-client-5.0.0.35269-macos_arm64.dmg", - "niceName": "Mac OS ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-server-5.0.0.35269-linux_arm64.deb", - "path": "arm/metavms-server-5.0.0.35269-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-linux_arm64.deb", - "path": "arm/metavms-client-5.0.0.35269-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-server-5.0.0.35269-linux_arm32.deb", - "path": "arm/metavms-server-5.0.0.35269-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-metadata_sdk-5.0.0.35269-universal.zip", - "path": "sdk/metavms-metadata_sdk-5.0.0.35269-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-storage_sdk-5.0.0.35269-universal.zip", - "path": "sdk/metavms-storage_sdk-5.0.0.35269-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - }, - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-video_source_sdk-5.0.0.35269-universal.zip", - "path": "sdk/metavms-video_source_sdk-5.0.0.35269-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - } - ], - "platforms": [ - { - "name": "linux", - "files": [ - { - "platform": "linux_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-linux_x64.deb", - "path": "linux/metavms-client-5.0.0.35269-linux_x64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-server-5.0.0.35269-linux_x64.deb", - "path": "linux/metavms-server-5.0.0.35269-linux_x64.deb", - "niceName": "Ubuntu x64 - Server installer" - }, - { - "platform": "linux_x64", - "appType": "vms_benchmark", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-vms_benchmark-5.0.0.35269-linux_x64.zip", - "path": "linux/metavms-vms_benchmark-5.0.0.35269-linux_x64.zip", - "niceName": "Ubuntu x64 - VMS Benchmark Tool" - } - ] - }, - { - "name": "windows", - "files": [ - { - "platform": "windows_x64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-bundle-5.0.0.35269-windows_x64.exe", - "path": "windows/metavms-bundle-5.0.0.35269-windows_x64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "windows_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-windows_x64.exe", - "path": "windows/metavms-client-5.0.0.35269-windows_x64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "windows_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-server-5.0.0.35269-windows_x64.exe", - "path": "windows/metavms-server-5.0.0.35269-windows_x64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-testcamera-5.0.0.35269-windows_x64.zip", - "path": "windows/metavms-testcamera-5.0.0.35269-windows_x64.zip", - "niceName": "Windows x64 - Test camera" - }, - { - "platform": "windows_x64", - "appType": "vms_benchmark", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-vms_benchmark-5.0.0.35269-windows_x64.zip", - "path": "windows/metavms-vms_benchmark-5.0.0.35269-windows_x64.zip", - "niceName": "Windows x64 - VMS Benchmark Tool" - } - ] - }, - { - "name": "macos", - "files": [ - { - "platform": "macos_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-macos_x64.dmg", - "path": "macos/metavms-client-5.0.0.35269-macos_x64.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "macos_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-macos_arm64.dmg", - "path": "macos/metavms-client-5.0.0.35269-macos_arm64.dmg", - "niceName": "Mac OS ARM64 - Client installer" - } - ] - }, - { - "name": "arm", - "files": [ - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-client-5.0.0.35269-linux_arm64.deb", - "path": "arm/metavms-client-5.0.0.35269-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-server-5.0.0.35269-linux_arm64.deb", - "path": "arm/metavms-server-5.0.0.35269-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-server-5.0.0.35269-linux_arm32.deb", - "path": "arm/metavms-server-5.0.0.35269-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - } - ] - }, - { - "name": "sdk", - "files": [ - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-video_source_sdk-5.0.0.35269-universal.zip", - "path": "sdk/metavms-video_source_sdk-5.0.0.35269-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-metadata_sdk-5.0.0.35269-universal.zip", - "path": "sdk/metavms-metadata_sdk-5.0.0.35269-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "metavms-storage_sdk-5.0.0.35269-universal.zip", - "path": "sdk/metavms-storage_sdk-5.0.0.35269-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - } - ] - } - ], - "backwardsCompatible": true, - "cloudGroup": "prod", - "beta": false, - "dismissed": false, - "releaseUrl": "https://updates.networkoptix.com/metavms/35269/" +{ + "version": "5.0.0.35269 R11", + "releaseNotes": "https://meta.nxvms.com/", + "product": "metavms", + "productDescription": "Video Management System", + "date": "26 Jul 2022", + "buildNumber": "35269", + "password": "pvj899", + "type": "release", + "installers": [ + { + "platform": "linux_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-linux_x64.deb", + "path": "linux/metavms-client-5.0.0.35269-linux_x64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-server-5.0.0.35269-linux_x64.deb", + "path": "linux/metavms-server-5.0.0.35269-linux_x64.deb", + "niceName": "Ubuntu x64 - Server installer" + }, + { + "platform": "linux_x64", + "appType": "vms_benchmark", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-vms_benchmark-5.0.0.35269-linux_x64.zip", + "path": "linux/metavms-vms_benchmark-5.0.0.35269-linux_x64.zip", + "niceName": "Ubuntu x64 - VMS Benchmark Tool" + }, + { + "platform": "windows_x64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-bundle-5.0.0.35269-windows_x64.exe", + "path": "windows/metavms-bundle-5.0.0.35269-windows_x64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "windows_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-windows_x64.exe", + "path": "windows/metavms-client-5.0.0.35269-windows_x64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "windows_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-server-5.0.0.35269-windows_x64.exe", + "path": "windows/metavms-server-5.0.0.35269-windows_x64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-testcamera-5.0.0.35269-windows_x64.zip", + "path": "windows/metavms-testcamera-5.0.0.35269-windows_x64.zip", + "niceName": "Windows x64 - Test camera" + }, + { + "platform": "windows_x64", + "appType": "vms_benchmark", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-vms_benchmark-5.0.0.35269-windows_x64.zip", + "path": "windows/metavms-vms_benchmark-5.0.0.35269-windows_x64.zip", + "niceName": "Windows x64 - VMS Benchmark Tool" + }, + { + "platform": "macos_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-macos_x64.dmg", + "path": "macos/metavms-client-5.0.0.35269-macos_x64.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "macos_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-macos_arm64.dmg", + "path": "macos/metavms-client-5.0.0.35269-macos_arm64.dmg", + "niceName": "Mac OS ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-server-5.0.0.35269-linux_arm64.deb", + "path": "arm/metavms-server-5.0.0.35269-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-linux_arm64.deb", + "path": "arm/metavms-client-5.0.0.35269-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-server-5.0.0.35269-linux_arm32.deb", + "path": "arm/metavms-server-5.0.0.35269-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-metadata_sdk-5.0.0.35269-universal.zip", + "path": "sdk/metavms-metadata_sdk-5.0.0.35269-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-storage_sdk-5.0.0.35269-universal.zip", + "path": "sdk/metavms-storage_sdk-5.0.0.35269-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + }, + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-video_source_sdk-5.0.0.35269-universal.zip", + "path": "sdk/metavms-video_source_sdk-5.0.0.35269-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + } + ], + "platforms": [ + { + "name": "linux", + "files": [ + { + "platform": "linux_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-linux_x64.deb", + "path": "linux/metavms-client-5.0.0.35269-linux_x64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-server-5.0.0.35269-linux_x64.deb", + "path": "linux/metavms-server-5.0.0.35269-linux_x64.deb", + "niceName": "Ubuntu x64 - Server installer" + }, + { + "platform": "linux_x64", + "appType": "vms_benchmark", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-vms_benchmark-5.0.0.35269-linux_x64.zip", + "path": "linux/metavms-vms_benchmark-5.0.0.35269-linux_x64.zip", + "niceName": "Ubuntu x64 - VMS Benchmark Tool" + } + ] + }, + { + "name": "windows", + "files": [ + { + "platform": "windows_x64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-bundle-5.0.0.35269-windows_x64.exe", + "path": "windows/metavms-bundle-5.0.0.35269-windows_x64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "windows_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-windows_x64.exe", + "path": "windows/metavms-client-5.0.0.35269-windows_x64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "windows_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-server-5.0.0.35269-windows_x64.exe", + "path": "windows/metavms-server-5.0.0.35269-windows_x64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-testcamera-5.0.0.35269-windows_x64.zip", + "path": "windows/metavms-testcamera-5.0.0.35269-windows_x64.zip", + "niceName": "Windows x64 - Test camera" + }, + { + "platform": "windows_x64", + "appType": "vms_benchmark", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-vms_benchmark-5.0.0.35269-windows_x64.zip", + "path": "windows/metavms-vms_benchmark-5.0.0.35269-windows_x64.zip", + "niceName": "Windows x64 - VMS Benchmark Tool" + } + ] + }, + { + "name": "macos", + "files": [ + { + "platform": "macos_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-macos_x64.dmg", + "path": "macos/metavms-client-5.0.0.35269-macos_x64.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "macos_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-macos_arm64.dmg", + "path": "macos/metavms-client-5.0.0.35269-macos_arm64.dmg", + "niceName": "Mac OS ARM64 - Client installer" + } + ] + }, + { + "name": "arm", + "files": [ + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-client-5.0.0.35269-linux_arm64.deb", + "path": "arm/metavms-client-5.0.0.35269-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-server-5.0.0.35269-linux_arm64.deb", + "path": "arm/metavms-server-5.0.0.35269-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-server-5.0.0.35269-linux_arm32.deb", + "path": "arm/metavms-server-5.0.0.35269-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + } + ] + }, + { + "name": "sdk", + "files": [ + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-video_source_sdk-5.0.0.35269-universal.zip", + "path": "sdk/metavms-video_source_sdk-5.0.0.35269-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-metadata_sdk-5.0.0.35269-universal.zip", + "path": "sdk/metavms-metadata_sdk-5.0.0.35269-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "metavms-storage_sdk-5.0.0.35269-universal.zip", + "path": "sdk/metavms-storage_sdk-5.0.0.35269-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + } + ] + } + ], + "backwardsCompatible": true, + "cloudGroup": "prod", + "beta": false, + "dismissed": false, + "releaseUrl": "https://updates.networkoptix.com/metavms/35269/" } \ No newline at end of file diff --git a/Samples/Downloads/NxWitness.json b/Samples/Downloads/NxWitness.json index 41246ff7..4c85d2b8 100644 --- a/Samples/Downloads/NxWitness.json +++ b/Samples/Downloads/NxWitness.json @@ -1,350 +1,350 @@ -{ - "version": "5.0.0.35270", - "releaseNotes": "https://www.networkoptix.com/all-nx-witness-release-notes", - "product": "nxwitness", - "productDescription": "Video Management System", - "date": "26 Jul 2022", - "buildNumber": "35270", - "password": "xk3y79", - "type": "release", - "installers": [ - { - "platform": "linux_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-linux_x64.deb", - "path": "linux/nxwitness-client-5.0.0.35270-linux_x64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-linux_x64.deb", - "path": "linux/nxwitness-server-5.0.0.35270-linux_x64.deb", - "niceName": "Ubuntu x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-bundle-5.0.0.35270-windows_x64.exe", - "path": "windows/nxwitness-bundle-5.0.0.35270-windows_x64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "windows_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-windows_x64.exe", - "path": "windows/nxwitness-client-5.0.0.35270-windows_x64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.35270-Net2v5.exe", - "path": "windows/nxwitness-paxton_plugin-5.0.0.35270-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.35270-Net2v6.exe", - "path": "windows/nxwitness-paxton_plugin-5.0.0.35270-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "windows_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-windows_x64.exe", - "path": "windows/nxwitness-server-5.0.0.35270-windows_x64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-testcamera-5.0.0.35270-windows_x64.zip", - "path": "windows/nxwitness-testcamera-5.0.0.35270-windows_x64.zip", - "niceName": "Windows x64 - Test camera" - }, - { - "platform": "macos_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-macos_x64.dmg", - "path": "macos/nxwitness-client-5.0.0.35270-macos_x64.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "macos_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-macos_arm64.dmg", - "path": "macos/nxwitness-client-5.0.0.35270-macos_arm64.dmg", - "niceName": "Mac OS ARM64 - Client installer" - }, - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-edge1.zip", - "path": "arm/nxwitness-server-5.0.0.35270-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-linux_arm64.deb", - "path": "arm/nxwitness-server-5.0.0.35270-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-linux_arm64.deb", - "path": "arm/nxwitness-client-5.0.0.35270-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-linux_arm32.deb", - "path": "arm/nxwitness-server-5.0.0.35270-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-metadata_sdk-5.0.0.35270-universal.zip", - "path": "sdk/nxwitness-metadata_sdk-5.0.0.35270-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-storage_sdk-5.0.0.35270-universal.zip", - "path": "sdk/nxwitness-storage_sdk-5.0.0.35270-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - }, - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-video_source_sdk-5.0.0.35270-universal.zip", - "path": "sdk/nxwitness-video_source_sdk-5.0.0.35270-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - } - ], - "platforms": [ - { - "name": "linux", - "files": [ - { - "platform": "linux_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-linux_x64.deb", - "path": "linux/nxwitness-client-5.0.0.35270-linux_x64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-linux_x64.deb", - "path": "linux/nxwitness-server-5.0.0.35270-linux_x64.deb", - "niceName": "Ubuntu x64 - Server installer" - } - ] - }, - { - "name": "windows", - "files": [ - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.35270-Net2v5.exe", - "path": "windows/nxwitness-paxton_plugin-5.0.0.35270-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.35270-Net2v6.exe", - "path": "windows/nxwitness-paxton_plugin-5.0.0.35270-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "windows_x64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-bundle-5.0.0.35270-windows_x64.exe", - "path": "windows/nxwitness-bundle-5.0.0.35270-windows_x64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "windows_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-windows_x64.exe", - "path": "windows/nxwitness-client-5.0.0.35270-windows_x64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "windows_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-windows_x64.exe", - "path": "windows/nxwitness-server-5.0.0.35270-windows_x64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-testcamera-5.0.0.35270-windows_x64.zip", - "path": "windows/nxwitness-testcamera-5.0.0.35270-windows_x64.zip", - "niceName": "Windows x64 - Test camera" - } - ] - }, - { - "name": "macos", - "files": [ - { - "platform": "macos_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-macos_x64.dmg", - "path": "macos/nxwitness-client-5.0.0.35270-macos_x64.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "macos_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-macos_arm64.dmg", - "path": "macos/nxwitness-client-5.0.0.35270-macos_arm64.dmg", - "niceName": "Mac OS ARM64 - Client installer" - } - ] - }, - { - "name": "arm", - "files": [ - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-edge1.zip", - "path": "arm/nxwitness-server-5.0.0.35270-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.35270-linux_arm64.deb", - "path": "arm/nxwitness-client-5.0.0.35270-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-linux_arm64.deb", - "path": "arm/nxwitness-server-5.0.0.35270-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.35270-linux_arm32.deb", - "path": "arm/nxwitness-server-5.0.0.35270-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - } - ] - }, - { - "name": "sdk", - "files": [ - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-video_source_sdk-5.0.0.35270-universal.zip", - "path": "sdk/nxwitness-video_source_sdk-5.0.0.35270-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-metadata_sdk-5.0.0.35270-universal.zip", - "path": "sdk/nxwitness-metadata_sdk-5.0.0.35270-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-storage_sdk-5.0.0.35270-universal.zip", - "path": "sdk/nxwitness-storage_sdk-5.0.0.35270-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - } - ] - } - ], - "backwardsCompatible": true, - "cloudGroup": "prod", - "beta": false, - "dismissed": false, - "releaseUrl": "https://updates.networkoptix.com/default/35270/" +{ + "version": "5.0.0.35270", + "releaseNotes": "https://www.networkoptix.com/all-nx-witness-release-notes", + "product": "nxwitness", + "productDescription": "Video Management System", + "date": "26 Jul 2022", + "buildNumber": "35270", + "password": "xk3y79", + "type": "release", + "installers": [ + { + "platform": "linux_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-linux_x64.deb", + "path": "linux/nxwitness-client-5.0.0.35270-linux_x64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-linux_x64.deb", + "path": "linux/nxwitness-server-5.0.0.35270-linux_x64.deb", + "niceName": "Ubuntu x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-bundle-5.0.0.35270-windows_x64.exe", + "path": "windows/nxwitness-bundle-5.0.0.35270-windows_x64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "windows_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-windows_x64.exe", + "path": "windows/nxwitness-client-5.0.0.35270-windows_x64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.35270-Net2v5.exe", + "path": "windows/nxwitness-paxton_plugin-5.0.0.35270-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.35270-Net2v6.exe", + "path": "windows/nxwitness-paxton_plugin-5.0.0.35270-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "windows_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-windows_x64.exe", + "path": "windows/nxwitness-server-5.0.0.35270-windows_x64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-testcamera-5.0.0.35270-windows_x64.zip", + "path": "windows/nxwitness-testcamera-5.0.0.35270-windows_x64.zip", + "niceName": "Windows x64 - Test camera" + }, + { + "platform": "macos_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-macos_x64.dmg", + "path": "macos/nxwitness-client-5.0.0.35270-macos_x64.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "macos_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-macos_arm64.dmg", + "path": "macos/nxwitness-client-5.0.0.35270-macos_arm64.dmg", + "niceName": "Mac OS ARM64 - Client installer" + }, + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-edge1.zip", + "path": "arm/nxwitness-server-5.0.0.35270-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-linux_arm64.deb", + "path": "arm/nxwitness-server-5.0.0.35270-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-linux_arm64.deb", + "path": "arm/nxwitness-client-5.0.0.35270-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-linux_arm32.deb", + "path": "arm/nxwitness-server-5.0.0.35270-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-metadata_sdk-5.0.0.35270-universal.zip", + "path": "sdk/nxwitness-metadata_sdk-5.0.0.35270-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-storage_sdk-5.0.0.35270-universal.zip", + "path": "sdk/nxwitness-storage_sdk-5.0.0.35270-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + }, + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-video_source_sdk-5.0.0.35270-universal.zip", + "path": "sdk/nxwitness-video_source_sdk-5.0.0.35270-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + } + ], + "platforms": [ + { + "name": "linux", + "files": [ + { + "platform": "linux_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-linux_x64.deb", + "path": "linux/nxwitness-client-5.0.0.35270-linux_x64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-linux_x64.deb", + "path": "linux/nxwitness-server-5.0.0.35270-linux_x64.deb", + "niceName": "Ubuntu x64 - Server installer" + } + ] + }, + { + "name": "windows", + "files": [ + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.35270-Net2v5.exe", + "path": "windows/nxwitness-paxton_plugin-5.0.0.35270-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.35270-Net2v6.exe", + "path": "windows/nxwitness-paxton_plugin-5.0.0.35270-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "windows_x64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-bundle-5.0.0.35270-windows_x64.exe", + "path": "windows/nxwitness-bundle-5.0.0.35270-windows_x64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "windows_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-windows_x64.exe", + "path": "windows/nxwitness-client-5.0.0.35270-windows_x64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "windows_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-windows_x64.exe", + "path": "windows/nxwitness-server-5.0.0.35270-windows_x64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-testcamera-5.0.0.35270-windows_x64.zip", + "path": "windows/nxwitness-testcamera-5.0.0.35270-windows_x64.zip", + "niceName": "Windows x64 - Test camera" + } + ] + }, + { + "name": "macos", + "files": [ + { + "platform": "macos_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-macos_x64.dmg", + "path": "macos/nxwitness-client-5.0.0.35270-macos_x64.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "macos_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-macos_arm64.dmg", + "path": "macos/nxwitness-client-5.0.0.35270-macos_arm64.dmg", + "niceName": "Mac OS ARM64 - Client installer" + } + ] + }, + { + "name": "arm", + "files": [ + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-edge1.zip", + "path": "arm/nxwitness-server-5.0.0.35270-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.35270-linux_arm64.deb", + "path": "arm/nxwitness-client-5.0.0.35270-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-linux_arm64.deb", + "path": "arm/nxwitness-server-5.0.0.35270-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.35270-linux_arm32.deb", + "path": "arm/nxwitness-server-5.0.0.35270-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + } + ] + }, + { + "name": "sdk", + "files": [ + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-video_source_sdk-5.0.0.35270-universal.zip", + "path": "sdk/nxwitness-video_source_sdk-5.0.0.35270-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-metadata_sdk-5.0.0.35270-universal.zip", + "path": "sdk/nxwitness-metadata_sdk-5.0.0.35270-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-storage_sdk-5.0.0.35270-universal.zip", + "path": "sdk/nxwitness-storage_sdk-5.0.0.35270-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + } + ] + } + ], + "backwardsCompatible": true, + "cloudGroup": "prod", + "beta": false, + "dismissed": false, + "releaseUrl": "https://updates.networkoptix.com/default/35270/" } \ No newline at end of file diff --git a/Samples/Downloads/Nxwitness.1.json b/Samples/Downloads/Nxwitness.1.json index 6f3ccabf..907239d6 100644 --- a/Samples/Downloads/Nxwitness.1.json +++ b/Samples/Downloads/Nxwitness.1.json @@ -1,927 +1,927 @@ -{ - "version": "5.0.0.36634", - "meta_version": "P1", - "releaseNotes": "https://www.networkoptix.com/all-nx-witness-release-notes", - "product": "nxwitness", - "productDescription": "Video Management System", - "date": "04 Apr 2023", - "buildNumber": "36634", - "password": "thn04r", - "type": "release", - "backwardCompatible": true, - "cloudGroup": "prod", - "beta": false, - "dismissed": false, - "installers": [ - { - "platform": "edge1", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-edge1.txt", - "path": "arm/nxwitness-conan_refs-5.0.0.36634-edge1.txt", - "niceName": "Edge - conan_refs" - }, - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-edge1.zip", - "path": "arm/nxwitness-server-5.0.0.36634-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "edge1", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-edge1.zip", - "path": "nxwitness-server_update-5.0.0.36634-edge1.zip", - "niceName": "Edge - Update package - server" - }, - { - "platform": "linux_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-linux_x64.deb", - "path": "linux/nxwitness-client-5.0.0.36634-linux_x64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux_x64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-linux_x64.zip", - "path": "nxwitness-client_update-5.0.0.36634-linux_x64.zip", - "niceName": "Ubuntu x64 - Update package - client" - }, - { - "platform": "linux_x64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_x64.txt", - "path": "linux/nxwitness-conan_refs-5.0.0.36634-linux_x64.txt", - "niceName": "Ubuntu x64 - conan_refs" - }, - { - "platform": "linux_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-linux_x64.deb", - "path": "linux/nxwitness-server-5.0.0.36634-linux_x64.deb", - "niceName": "Ubuntu x64 - Server installer" - }, - { - "platform": "linux_x64", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-linux_x64.zip", - "path": "nxwitness-server_update-5.0.0.36634-linux_x64.zip", - "niceName": "Ubuntu x64 - Update package - server" - }, - { - "platform": "linux_arm32", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_arm32.txt", - "path": "arm/nxwitness-conan_refs-5.0.0.36634-linux_arm32.txt", - "niceName": "Raspberry Pi / ARM32 - conan_refs" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-linux_arm32.deb", - "path": "arm/nxwitness-server-5.0.0.36634-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - }, - { - "platform": "bananapi", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-bananapi.zip", - "path": "nxwitness-server_update-5.0.0.36634-bananapi.zip", - "niceName": "Banana Pi - Update package - server" - }, - { - "platform": "linux_arm32", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-linux_arm32.zip", - "path": "nxwitness-server_update-5.0.0.36634-linux_arm32.zip", - "niceName": "Raspberry Pi / ARM32 - Update package - server" - }, - { - "platform": "rpi", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-rpi.zip", - "path": "nxwitness-server_update-5.0.0.36634-rpi.zip", - "niceName": "Raspberry Pi - Update package - server" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-linux_arm64.deb", - "path": "arm/nxwitness-client-5.0.0.36634-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-linux_arm64.zip", - "path": "nxwitness-client_update-5.0.0.36634-linux_arm64.zip", - "niceName": "NVIDIA Jetson / ARM64 - Update package - client" - }, - { - "platform": "linux_arm64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_arm64.txt", - "path": "arm/nxwitness-conan_refs-5.0.0.36634-linux_arm64.txt", - "niceName": "NVIDIA Jetson / ARM64 - conan_refs" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-linux_arm64.deb", - "path": "arm/nxwitness-server-5.0.0.36634-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-linux_arm64.zip", - "path": "nxwitness-server_update-5.0.0.36634-linux_arm64.zip", - "niceName": "NVIDIA Jetson / ARM64 - Update package - server" - }, - { - "platform": "macos_x64", - "appType": "client_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_debug-5.0.0.36634-macos_x64.zip", - "path": "macos/nxwitness-client_debug-5.0.0.36634-macos_x64.zip", - "niceName": "Mac OS - ~ PDB - client debug files" - }, - { - "platform": "macos_x64", - "appType": "libs_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-libs_debug-5.0.0.36634-macos_x64.zip", - "path": "macos/nxwitness-libs_debug-5.0.0.36634-macos_x64.zip", - "niceName": "Mac OS - ~ PDB - libraries debug files" - }, - { - "platform": "macos_x64", - "appType": "misc_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-misc_debug-5.0.0.36634-macos_x64.zip", - "path": "macos/nxwitness-misc_debug-5.0.0.36634-macos_x64.zip", - "niceName": "Mac OS - ~ PDB - Applauncher, traytool & other debug files" - }, - { - "platform": "macos_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-macos_x64.dmg", - "path": "macos/nxwitness-client-5.0.0.36634-macos_x64.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "macos", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-macos.zip", - "path": "nxwitness-client_update-5.0.0.36634-macos.zip", - "niceName": "Mac OS - Update package - client" - }, - { - "platform": "macos_x64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-macos_x64.zip", - "path": "nxwitness-client_update-5.0.0.36634-macos_x64.zip", - "niceName": "Mac OS - Update package - client" - }, - { - "platform": "macos_x64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-macos_x64.txt", - "path": "macos/nxwitness-conan_refs-5.0.0.36634-macos_x64.txt", - "niceName": "Mac OS - conan_refs" - }, - { - "platform": "macos_arm64", - "appType": "client_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_debug-5.0.0.36634-macos_arm64.zip", - "path": "macos/nxwitness-client_debug-5.0.0.36634-macos_arm64.zip", - "niceName": "Mac OS ARM64 - ~ PDB - client debug files" - }, - { - "platform": "macos_arm64", - "appType": "libs_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-libs_debug-5.0.0.36634-macos_arm64.zip", - "path": "macos/nxwitness-libs_debug-5.0.0.36634-macos_arm64.zip", - "niceName": "Mac OS ARM64 - ~ PDB - libraries debug files" - }, - { - "platform": "macos_arm64", - "appType": "misc_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-misc_debug-5.0.0.36634-macos_arm64.zip", - "path": "macos/nxwitness-misc_debug-5.0.0.36634-macos_arm64.zip", - "niceName": "Mac OS ARM64 - ~ PDB - Applauncher, traytool & other debug files" - }, - { - "platform": "macos_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-macos_arm64.dmg", - "path": "macos/nxwitness-client-5.0.0.36634-macos_arm64.dmg", - "niceName": "Mac OS ARM64 - Client installer" - }, - { - "platform": "macos_arm64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-macos_arm64.zip", - "path": "nxwitness-client_update-5.0.0.36634-macos_arm64.zip", - "niceName": "Mac OS ARM64 - Update package - client" - }, - { - "platform": "macos_arm64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-macos_arm64.txt", - "path": "macos/nxwitness-conan_refs-5.0.0.36634-macos_arm64.txt", - "niceName": "Mac OS ARM64 - conan_refs" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-metadata_sdk-5.0.0.36634-universal.zip", - "path": "sdk/nxwitness-metadata_sdk-5.0.0.36634-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-storage_sdk-5.0.0.36634-universal.zip", - "path": "sdk/nxwitness-storage_sdk-5.0.0.36634-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - }, - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-video_source_sdk-5.0.0.36634-universal.zip", - "path": "sdk/nxwitness-video_source_sdk-5.0.0.36634-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - }, - { - "platform": "windows_x64", - "appType": "client_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_debug-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-client_debug-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - ~ PDB - client debug files" - }, - { - "platform": "windows_x64", - "appType": "libs_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-libs_debug-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-libs_debug-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - ~ PDB - libraries debug files" - }, - { - "platform": "windows_x64", - "appType": "misc_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-misc_debug-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-misc_debug-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - ~ PDB - Applauncher, traytool & other debug files" - }, - { - "platform": "windows_x64", - "appType": "server_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_debug-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-server_debug-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - ~ PDB - server debug files" - }, - { - "platform": "windows_x64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-bundle-5.0.0.36634-windows_x64.exe", - "path": "windows/nxwitness-bundle-5.0.0.36634-windows_x64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "windows_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-windows_x64.exe", - "path": "windows/nxwitness-client-5.0.0.36634-windows_x64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "windows_x64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-windows_x64.zip", - "path": "nxwitness-client_update-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - Update package - client" - }, - { - "platform": "windows_x64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-windows_x64.txt", - "path": "windows/nxwitness-conan_refs-5.0.0.36634-windows_x64.txt", - "niceName": "Windows x64 - conan_refs" - }, - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v5.exe", - "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v5.msi", - "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v5.msi", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v6.exe", - "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v6.msi", - "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v6.msi", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "windows_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-windows_x64.exe", - "path": "windows/nxwitness-server-5.0.0.36634-windows_x64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-windows_x64.zip", - "path": "nxwitness-server_update-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - Update package - server" - }, - { - "platform": "windows_x64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-testcamera-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-testcamera-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - Test camera" - } - ], - "platforms": [ - { - "name": "sdk", - "files": [ - { - "platform": "universal", - "appType": "video_source_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-video_source_sdk-5.0.0.36634-universal.zip", - "path": "sdk/nxwitness-video_source_sdk-5.0.0.36634-universal.zip", - "niceName": "Cross-platform - SDK: Camera" - }, - { - "platform": "universal", - "appType": "metadata_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-metadata_sdk-5.0.0.36634-universal.zip", - "path": "sdk/nxwitness-metadata_sdk-5.0.0.36634-universal.zip", - "niceName": "Cross-platform - SDK: Metadata" - }, - { - "platform": "universal", - "appType": "storage_sdk", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-storage_sdk-5.0.0.36634-universal.zip", - "path": "sdk/nxwitness-storage_sdk-5.0.0.36634-universal.zip", - "niceName": "Cross-platform - SDK: Storage" - } - ] - }, - { - "name": "arm", - "files": [ - { - "platform": "bananapi", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-bananapi.zip", - "path": "nxwitness-server_update-5.0.0.36634-bananapi.zip", - "niceName": "Banana Pi - Update package - server" - }, - { - "platform": "edge1", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-edge1.zip", - "path": "arm/nxwitness-server-5.0.0.36634-edge1.zip", - "niceName": "Edge - Server installer" - }, - { - "platform": "edge1", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-edge1.zip", - "path": "nxwitness-server_update-5.0.0.36634-edge1.zip", - "niceName": "Edge - Update package - server" - }, - { - "platform": "edge1", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-edge1.txt", - "path": "arm/nxwitness-conan_refs-5.0.0.36634-edge1.txt", - "niceName": "Edge - conan_refs" - }, - { - "platform": "linux_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-linux_arm64.deb", - "path": "arm/nxwitness-client-5.0.0.36634-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Client installer" - }, - { - "platform": "linux_arm64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-linux_arm64.deb", - "path": "arm/nxwitness-server-5.0.0.36634-linux_arm64.deb", - "niceName": "NVIDIA Jetson / ARM64 - Server installer" - }, - { - "platform": "linux_arm64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-linux_arm64.zip", - "path": "nxwitness-client_update-5.0.0.36634-linux_arm64.zip", - "niceName": "NVIDIA Jetson / ARM64 - Update package - client" - }, - { - "platform": "linux_arm64", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-linux_arm64.zip", - "path": "nxwitness-server_update-5.0.0.36634-linux_arm64.zip", - "niceName": "NVIDIA Jetson / ARM64 - Update package - server" - }, - { - "platform": "linux_arm64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_arm64.txt", - "path": "arm/nxwitness-conan_refs-5.0.0.36634-linux_arm64.txt", - "niceName": "NVIDIA Jetson / ARM64 - conan_refs" - }, - { - "platform": "rpi", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-rpi.zip", - "path": "nxwitness-server_update-5.0.0.36634-rpi.zip", - "niceName": "Raspberry Pi - Update package - server" - }, - { - "platform": "linux_arm32", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-linux_arm32.deb", - "path": "arm/nxwitness-server-5.0.0.36634-linux_arm32.deb", - "niceName": "Raspberry Pi / ARM32 - Server installer" - }, - { - "platform": "linux_arm32", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-linux_arm32.zip", - "path": "nxwitness-server_update-5.0.0.36634-linux_arm32.zip", - "niceName": "Raspberry Pi / ARM32 - Update package - server" - }, - { - "platform": "linux_arm32", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_arm32.txt", - "path": "arm/nxwitness-conan_refs-5.0.0.36634-linux_arm32.txt", - "niceName": "Raspberry Pi / ARM32 - conan_refs" - } - ] - }, - { - "name": "linux", - "files": [ - { - "platform": "linux_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-linux_x64.deb", - "path": "linux/nxwitness-client-5.0.0.36634-linux_x64.deb", - "niceName": "Ubuntu x64 - Client installer" - }, - { - "platform": "linux_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-linux_x64.deb", - "path": "linux/nxwitness-server-5.0.0.36634-linux_x64.deb", - "niceName": "Ubuntu x64 - Server installer" - }, - { - "platform": "linux_x64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-linux_x64.zip", - "path": "nxwitness-client_update-5.0.0.36634-linux_x64.zip", - "niceName": "Ubuntu x64 - Update package - client" - }, - { - "platform": "linux_x64", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-linux_x64.zip", - "path": "nxwitness-server_update-5.0.0.36634-linux_x64.zip", - "niceName": "Ubuntu x64 - Update package - server" - }, - { - "platform": "linux_x64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_x64.txt", - "path": "linux/nxwitness-conan_refs-5.0.0.36634-linux_x64.txt", - "niceName": "Ubuntu x64 - conan_refs" - } - ] - }, - { - "name": "macos", - "files": [ - { - "platform": "macos_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-macos_x64.dmg", - "path": "macos/nxwitness-client-5.0.0.36634-macos_x64.dmg", - "niceName": "Mac OS - Client installer" - }, - { - "platform": "macos", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-macos.zip", - "path": "nxwitness-client_update-5.0.0.36634-macos.zip", - "niceName": "Mac OS - Update package - client" - }, - { - "platform": "macos_x64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-macos_x64.zip", - "path": "nxwitness-client_update-5.0.0.36634-macos_x64.zip", - "niceName": "Mac OS - Update package - client" - }, - { - "platform": "macos_x64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-macos_x64.txt", - "path": "macos/nxwitness-conan_refs-5.0.0.36634-macos_x64.txt", - "niceName": "Mac OS - conan_refs" - }, - { - "platform": "macos_x64", - "appType": "misc_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-misc_debug-5.0.0.36634-macos_x64.zip", - "path": "macos/nxwitness-misc_debug-5.0.0.36634-macos_x64.zip", - "niceName": "Mac OS - ~ PDB - Applauncher, traytool & other debug files" - }, - { - "platform": "macos_x64", - "appType": "client_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_debug-5.0.0.36634-macos_x64.zip", - "path": "macos/nxwitness-client_debug-5.0.0.36634-macos_x64.zip", - "niceName": "Mac OS - ~ PDB - client debug files" - }, - { - "platform": "macos_x64", - "appType": "libs_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-libs_debug-5.0.0.36634-macos_x64.zip", - "path": "macos/nxwitness-libs_debug-5.0.0.36634-macos_x64.zip", - "niceName": "Mac OS - ~ PDB - libraries debug files" - }, - { - "platform": "macos_arm64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-macos_arm64.dmg", - "path": "macos/nxwitness-client-5.0.0.36634-macos_arm64.dmg", - "niceName": "Mac OS ARM64 - Client installer" - }, - { - "platform": "macos_arm64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-macos_arm64.zip", - "path": "nxwitness-client_update-5.0.0.36634-macos_arm64.zip", - "niceName": "Mac OS ARM64 - Update package - client" - }, - { - "platform": "macos_arm64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-macos_arm64.txt", - "path": "macos/nxwitness-conan_refs-5.0.0.36634-macos_arm64.txt", - "niceName": "Mac OS ARM64 - conan_refs" - }, - { - "platform": "macos_arm64", - "appType": "misc_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-misc_debug-5.0.0.36634-macos_arm64.zip", - "path": "macos/nxwitness-misc_debug-5.0.0.36634-macos_arm64.zip", - "niceName": "Mac OS ARM64 - ~ PDB - Applauncher, traytool & other debug files" - }, - { - "platform": "macos_arm64", - "appType": "client_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_debug-5.0.0.36634-macos_arm64.zip", - "path": "macos/nxwitness-client_debug-5.0.0.36634-macos_arm64.zip", - "niceName": "Mac OS ARM64 - ~ PDB - client debug files" - }, - { - "platform": "macos_arm64", - "appType": "libs_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-libs_debug-5.0.0.36634-macos_arm64.zip", - "path": "macos/nxwitness-libs_debug-5.0.0.36634-macos_arm64.zip", - "niceName": "Mac OS ARM64 - ~ PDB - libraries debug files" - } - ] - }, - { - "name": "windows", - "files": [ - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v5.exe", - "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v5.exe", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v5", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v5.msi", - "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v5.msi", - "niceName": "Paxton Net2 Pro v5 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v6.exe", - "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v6.exe", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "Net2v6", - "appType": "paxton_plugin", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v6.msi", - "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v6.msi", - "niceName": "Paxton Net2 Pro v6 - plugin installer" - }, - { - "platform": "windows_x64", - "appType": "bundle", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-bundle-5.0.0.36634-windows_x64.exe", - "path": "windows/nxwitness-bundle-5.0.0.36634-windows_x64.exe", - "niceName": "Windows x64 - Client & Server" - }, - { - "platform": "windows_x64", - "appType": "client", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client-5.0.0.36634-windows_x64.exe", - "path": "windows/nxwitness-client-5.0.0.36634-windows_x64.exe", - "niceName": "Windows x64 - Client installer" - }, - { - "platform": "windows_x64", - "appType": "server", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server-5.0.0.36634-windows_x64.exe", - "path": "windows/nxwitness-server-5.0.0.36634-windows_x64.exe", - "niceName": "Windows x64 - Server installer" - }, - { - "platform": "windows_x64", - "appType": "testcamera", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-testcamera-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-testcamera-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - Test camera" - }, - { - "platform": "windows_x64", - "appType": "client_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_update-5.0.0.36634-windows_x64.zip", - "path": "nxwitness-client_update-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - Update package - client" - }, - { - "platform": "windows_x64", - "appType": "server_update", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_update-5.0.0.36634-windows_x64.zip", - "path": "nxwitness-server_update-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - Update package - server" - }, - { - "platform": "windows_x64", - "appType": "conan_refs", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-conan_refs-5.0.0.36634-windows_x64.txt", - "path": "windows/nxwitness-conan_refs-5.0.0.36634-windows_x64.txt", - "niceName": "Windows x64 - conan_refs" - }, - { - "platform": "windows_x64", - "appType": "misc_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-misc_debug-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-misc_debug-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - ~ PDB - Applauncher, traytool & other debug files" - }, - { - "platform": "windows_x64", - "appType": "client_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-client_debug-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-client_debug-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - ~ PDB - client debug files" - }, - { - "platform": "windows_x64", - "appType": "libs_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-libs_debug-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-libs_debug-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - ~ PDB - libraries debug files" - }, - { - "platform": "windows_x64", - "appType": "server_debug", - "beta": false, - "cloudGroup": "prod", - "fileName": "nxwitness-server_debug-5.0.0.36634-windows_x64.zip", - "path": "windows/nxwitness-server_debug-5.0.0.36634-windows_x64.zip", - "niceName": "Windows x64 - ~ PDB - server debug files" - } - ] - } - ], - "releaseUrl": "https://updates.networkoptix.com/default/36634/" +{ + "version": "5.0.0.36634", + "meta_version": "P1", + "releaseNotes": "https://www.networkoptix.com/all-nx-witness-release-notes", + "product": "nxwitness", + "productDescription": "Video Management System", + "date": "04 Apr 2023", + "buildNumber": "36634", + "password": "thn04r", + "type": "release", + "backwardCompatible": true, + "cloudGroup": "prod", + "beta": false, + "dismissed": false, + "installers": [ + { + "platform": "edge1", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-edge1.txt", + "path": "arm/nxwitness-conan_refs-5.0.0.36634-edge1.txt", + "niceName": "Edge - conan_refs" + }, + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-edge1.zip", + "path": "arm/nxwitness-server-5.0.0.36634-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "edge1", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-edge1.zip", + "path": "nxwitness-server_update-5.0.0.36634-edge1.zip", + "niceName": "Edge - Update package - server" + }, + { + "platform": "linux_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-linux_x64.deb", + "path": "linux/nxwitness-client-5.0.0.36634-linux_x64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux_x64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-linux_x64.zip", + "path": "nxwitness-client_update-5.0.0.36634-linux_x64.zip", + "niceName": "Ubuntu x64 - Update package - client" + }, + { + "platform": "linux_x64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_x64.txt", + "path": "linux/nxwitness-conan_refs-5.0.0.36634-linux_x64.txt", + "niceName": "Ubuntu x64 - conan_refs" + }, + { + "platform": "linux_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-linux_x64.deb", + "path": "linux/nxwitness-server-5.0.0.36634-linux_x64.deb", + "niceName": "Ubuntu x64 - Server installer" + }, + { + "platform": "linux_x64", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-linux_x64.zip", + "path": "nxwitness-server_update-5.0.0.36634-linux_x64.zip", + "niceName": "Ubuntu x64 - Update package - server" + }, + { + "platform": "linux_arm32", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_arm32.txt", + "path": "arm/nxwitness-conan_refs-5.0.0.36634-linux_arm32.txt", + "niceName": "Raspberry Pi / ARM32 - conan_refs" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-linux_arm32.deb", + "path": "arm/nxwitness-server-5.0.0.36634-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + }, + { + "platform": "bananapi", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-bananapi.zip", + "path": "nxwitness-server_update-5.0.0.36634-bananapi.zip", + "niceName": "Banana Pi - Update package - server" + }, + { + "platform": "linux_arm32", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-linux_arm32.zip", + "path": "nxwitness-server_update-5.0.0.36634-linux_arm32.zip", + "niceName": "Raspberry Pi / ARM32 - Update package - server" + }, + { + "platform": "rpi", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-rpi.zip", + "path": "nxwitness-server_update-5.0.0.36634-rpi.zip", + "niceName": "Raspberry Pi - Update package - server" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-linux_arm64.deb", + "path": "arm/nxwitness-client-5.0.0.36634-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-linux_arm64.zip", + "path": "nxwitness-client_update-5.0.0.36634-linux_arm64.zip", + "niceName": "NVIDIA Jetson / ARM64 - Update package - client" + }, + { + "platform": "linux_arm64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_arm64.txt", + "path": "arm/nxwitness-conan_refs-5.0.0.36634-linux_arm64.txt", + "niceName": "NVIDIA Jetson / ARM64 - conan_refs" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-linux_arm64.deb", + "path": "arm/nxwitness-server-5.0.0.36634-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-linux_arm64.zip", + "path": "nxwitness-server_update-5.0.0.36634-linux_arm64.zip", + "niceName": "NVIDIA Jetson / ARM64 - Update package - server" + }, + { + "platform": "macos_x64", + "appType": "client_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_debug-5.0.0.36634-macos_x64.zip", + "path": "macos/nxwitness-client_debug-5.0.0.36634-macos_x64.zip", + "niceName": "Mac OS - ~ PDB - client debug files" + }, + { + "platform": "macos_x64", + "appType": "libs_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-libs_debug-5.0.0.36634-macos_x64.zip", + "path": "macos/nxwitness-libs_debug-5.0.0.36634-macos_x64.zip", + "niceName": "Mac OS - ~ PDB - libraries debug files" + }, + { + "platform": "macos_x64", + "appType": "misc_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-misc_debug-5.0.0.36634-macos_x64.zip", + "path": "macos/nxwitness-misc_debug-5.0.0.36634-macos_x64.zip", + "niceName": "Mac OS - ~ PDB - Applauncher, traytool & other debug files" + }, + { + "platform": "macos_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-macos_x64.dmg", + "path": "macos/nxwitness-client-5.0.0.36634-macos_x64.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "macos", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-macos.zip", + "path": "nxwitness-client_update-5.0.0.36634-macos.zip", + "niceName": "Mac OS - Update package - client" + }, + { + "platform": "macos_x64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-macos_x64.zip", + "path": "nxwitness-client_update-5.0.0.36634-macos_x64.zip", + "niceName": "Mac OS - Update package - client" + }, + { + "platform": "macos_x64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-macos_x64.txt", + "path": "macos/nxwitness-conan_refs-5.0.0.36634-macos_x64.txt", + "niceName": "Mac OS - conan_refs" + }, + { + "platform": "macos_arm64", + "appType": "client_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_debug-5.0.0.36634-macos_arm64.zip", + "path": "macos/nxwitness-client_debug-5.0.0.36634-macos_arm64.zip", + "niceName": "Mac OS ARM64 - ~ PDB - client debug files" + }, + { + "platform": "macos_arm64", + "appType": "libs_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-libs_debug-5.0.0.36634-macos_arm64.zip", + "path": "macos/nxwitness-libs_debug-5.0.0.36634-macos_arm64.zip", + "niceName": "Mac OS ARM64 - ~ PDB - libraries debug files" + }, + { + "platform": "macos_arm64", + "appType": "misc_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-misc_debug-5.0.0.36634-macos_arm64.zip", + "path": "macos/nxwitness-misc_debug-5.0.0.36634-macos_arm64.zip", + "niceName": "Mac OS ARM64 - ~ PDB - Applauncher, traytool & other debug files" + }, + { + "platform": "macos_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-macos_arm64.dmg", + "path": "macos/nxwitness-client-5.0.0.36634-macos_arm64.dmg", + "niceName": "Mac OS ARM64 - Client installer" + }, + { + "platform": "macos_arm64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-macos_arm64.zip", + "path": "nxwitness-client_update-5.0.0.36634-macos_arm64.zip", + "niceName": "Mac OS ARM64 - Update package - client" + }, + { + "platform": "macos_arm64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-macos_arm64.txt", + "path": "macos/nxwitness-conan_refs-5.0.0.36634-macos_arm64.txt", + "niceName": "Mac OS ARM64 - conan_refs" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-metadata_sdk-5.0.0.36634-universal.zip", + "path": "sdk/nxwitness-metadata_sdk-5.0.0.36634-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-storage_sdk-5.0.0.36634-universal.zip", + "path": "sdk/nxwitness-storage_sdk-5.0.0.36634-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + }, + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-video_source_sdk-5.0.0.36634-universal.zip", + "path": "sdk/nxwitness-video_source_sdk-5.0.0.36634-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + }, + { + "platform": "windows_x64", + "appType": "client_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_debug-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-client_debug-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - ~ PDB - client debug files" + }, + { + "platform": "windows_x64", + "appType": "libs_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-libs_debug-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-libs_debug-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - ~ PDB - libraries debug files" + }, + { + "platform": "windows_x64", + "appType": "misc_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-misc_debug-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-misc_debug-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - ~ PDB - Applauncher, traytool & other debug files" + }, + { + "platform": "windows_x64", + "appType": "server_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_debug-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-server_debug-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - ~ PDB - server debug files" + }, + { + "platform": "windows_x64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-bundle-5.0.0.36634-windows_x64.exe", + "path": "windows/nxwitness-bundle-5.0.0.36634-windows_x64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "windows_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-windows_x64.exe", + "path": "windows/nxwitness-client-5.0.0.36634-windows_x64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "windows_x64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-windows_x64.zip", + "path": "nxwitness-client_update-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - Update package - client" + }, + { + "platform": "windows_x64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-windows_x64.txt", + "path": "windows/nxwitness-conan_refs-5.0.0.36634-windows_x64.txt", + "niceName": "Windows x64 - conan_refs" + }, + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v5.exe", + "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v5.msi", + "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v5.msi", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v6.exe", + "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v6.msi", + "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v6.msi", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "windows_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-windows_x64.exe", + "path": "windows/nxwitness-server-5.0.0.36634-windows_x64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-windows_x64.zip", + "path": "nxwitness-server_update-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - Update package - server" + }, + { + "platform": "windows_x64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-testcamera-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-testcamera-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - Test camera" + } + ], + "platforms": [ + { + "name": "sdk", + "files": [ + { + "platform": "universal", + "appType": "video_source_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-video_source_sdk-5.0.0.36634-universal.zip", + "path": "sdk/nxwitness-video_source_sdk-5.0.0.36634-universal.zip", + "niceName": "Cross-platform - SDK: Camera" + }, + { + "platform": "universal", + "appType": "metadata_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-metadata_sdk-5.0.0.36634-universal.zip", + "path": "sdk/nxwitness-metadata_sdk-5.0.0.36634-universal.zip", + "niceName": "Cross-platform - SDK: Metadata" + }, + { + "platform": "universal", + "appType": "storage_sdk", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-storage_sdk-5.0.0.36634-universal.zip", + "path": "sdk/nxwitness-storage_sdk-5.0.0.36634-universal.zip", + "niceName": "Cross-platform - SDK: Storage" + } + ] + }, + { + "name": "arm", + "files": [ + { + "platform": "bananapi", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-bananapi.zip", + "path": "nxwitness-server_update-5.0.0.36634-bananapi.zip", + "niceName": "Banana Pi - Update package - server" + }, + { + "platform": "edge1", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-edge1.zip", + "path": "arm/nxwitness-server-5.0.0.36634-edge1.zip", + "niceName": "Edge - Server installer" + }, + { + "platform": "edge1", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-edge1.zip", + "path": "nxwitness-server_update-5.0.0.36634-edge1.zip", + "niceName": "Edge - Update package - server" + }, + { + "platform": "edge1", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-edge1.txt", + "path": "arm/nxwitness-conan_refs-5.0.0.36634-edge1.txt", + "niceName": "Edge - conan_refs" + }, + { + "platform": "linux_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-linux_arm64.deb", + "path": "arm/nxwitness-client-5.0.0.36634-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Client installer" + }, + { + "platform": "linux_arm64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-linux_arm64.deb", + "path": "arm/nxwitness-server-5.0.0.36634-linux_arm64.deb", + "niceName": "NVIDIA Jetson / ARM64 - Server installer" + }, + { + "platform": "linux_arm64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-linux_arm64.zip", + "path": "nxwitness-client_update-5.0.0.36634-linux_arm64.zip", + "niceName": "NVIDIA Jetson / ARM64 - Update package - client" + }, + { + "platform": "linux_arm64", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-linux_arm64.zip", + "path": "nxwitness-server_update-5.0.0.36634-linux_arm64.zip", + "niceName": "NVIDIA Jetson / ARM64 - Update package - server" + }, + { + "platform": "linux_arm64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_arm64.txt", + "path": "arm/nxwitness-conan_refs-5.0.0.36634-linux_arm64.txt", + "niceName": "NVIDIA Jetson / ARM64 - conan_refs" + }, + { + "platform": "rpi", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-rpi.zip", + "path": "nxwitness-server_update-5.0.0.36634-rpi.zip", + "niceName": "Raspberry Pi - Update package - server" + }, + { + "platform": "linux_arm32", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-linux_arm32.deb", + "path": "arm/nxwitness-server-5.0.0.36634-linux_arm32.deb", + "niceName": "Raspberry Pi / ARM32 - Server installer" + }, + { + "platform": "linux_arm32", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-linux_arm32.zip", + "path": "nxwitness-server_update-5.0.0.36634-linux_arm32.zip", + "niceName": "Raspberry Pi / ARM32 - Update package - server" + }, + { + "platform": "linux_arm32", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_arm32.txt", + "path": "arm/nxwitness-conan_refs-5.0.0.36634-linux_arm32.txt", + "niceName": "Raspberry Pi / ARM32 - conan_refs" + } + ] + }, + { + "name": "linux", + "files": [ + { + "platform": "linux_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-linux_x64.deb", + "path": "linux/nxwitness-client-5.0.0.36634-linux_x64.deb", + "niceName": "Ubuntu x64 - Client installer" + }, + { + "platform": "linux_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-linux_x64.deb", + "path": "linux/nxwitness-server-5.0.0.36634-linux_x64.deb", + "niceName": "Ubuntu x64 - Server installer" + }, + { + "platform": "linux_x64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-linux_x64.zip", + "path": "nxwitness-client_update-5.0.0.36634-linux_x64.zip", + "niceName": "Ubuntu x64 - Update package - client" + }, + { + "platform": "linux_x64", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-linux_x64.zip", + "path": "nxwitness-server_update-5.0.0.36634-linux_x64.zip", + "niceName": "Ubuntu x64 - Update package - server" + }, + { + "platform": "linux_x64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-linux_x64.txt", + "path": "linux/nxwitness-conan_refs-5.0.0.36634-linux_x64.txt", + "niceName": "Ubuntu x64 - conan_refs" + } + ] + }, + { + "name": "macos", + "files": [ + { + "platform": "macos_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-macos_x64.dmg", + "path": "macos/nxwitness-client-5.0.0.36634-macos_x64.dmg", + "niceName": "Mac OS - Client installer" + }, + { + "platform": "macos", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-macos.zip", + "path": "nxwitness-client_update-5.0.0.36634-macos.zip", + "niceName": "Mac OS - Update package - client" + }, + { + "platform": "macos_x64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-macos_x64.zip", + "path": "nxwitness-client_update-5.0.0.36634-macos_x64.zip", + "niceName": "Mac OS - Update package - client" + }, + { + "platform": "macos_x64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-macos_x64.txt", + "path": "macos/nxwitness-conan_refs-5.0.0.36634-macos_x64.txt", + "niceName": "Mac OS - conan_refs" + }, + { + "platform": "macos_x64", + "appType": "misc_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-misc_debug-5.0.0.36634-macos_x64.zip", + "path": "macos/nxwitness-misc_debug-5.0.0.36634-macos_x64.zip", + "niceName": "Mac OS - ~ PDB - Applauncher, traytool & other debug files" + }, + { + "platform": "macos_x64", + "appType": "client_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_debug-5.0.0.36634-macos_x64.zip", + "path": "macos/nxwitness-client_debug-5.0.0.36634-macos_x64.zip", + "niceName": "Mac OS - ~ PDB - client debug files" + }, + { + "platform": "macos_x64", + "appType": "libs_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-libs_debug-5.0.0.36634-macos_x64.zip", + "path": "macos/nxwitness-libs_debug-5.0.0.36634-macos_x64.zip", + "niceName": "Mac OS - ~ PDB - libraries debug files" + }, + { + "platform": "macos_arm64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-macos_arm64.dmg", + "path": "macos/nxwitness-client-5.0.0.36634-macos_arm64.dmg", + "niceName": "Mac OS ARM64 - Client installer" + }, + { + "platform": "macos_arm64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-macos_arm64.zip", + "path": "nxwitness-client_update-5.0.0.36634-macos_arm64.zip", + "niceName": "Mac OS ARM64 - Update package - client" + }, + { + "platform": "macos_arm64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-macos_arm64.txt", + "path": "macos/nxwitness-conan_refs-5.0.0.36634-macos_arm64.txt", + "niceName": "Mac OS ARM64 - conan_refs" + }, + { + "platform": "macos_arm64", + "appType": "misc_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-misc_debug-5.0.0.36634-macos_arm64.zip", + "path": "macos/nxwitness-misc_debug-5.0.0.36634-macos_arm64.zip", + "niceName": "Mac OS ARM64 - ~ PDB - Applauncher, traytool & other debug files" + }, + { + "platform": "macos_arm64", + "appType": "client_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_debug-5.0.0.36634-macos_arm64.zip", + "path": "macos/nxwitness-client_debug-5.0.0.36634-macos_arm64.zip", + "niceName": "Mac OS ARM64 - ~ PDB - client debug files" + }, + { + "platform": "macos_arm64", + "appType": "libs_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-libs_debug-5.0.0.36634-macos_arm64.zip", + "path": "macos/nxwitness-libs_debug-5.0.0.36634-macos_arm64.zip", + "niceName": "Mac OS ARM64 - ~ PDB - libraries debug files" + } + ] + }, + { + "name": "windows", + "files": [ + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v5.exe", + "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v5.exe", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v5", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v5.msi", + "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v5.msi", + "niceName": "Paxton Net2 Pro v5 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v6.exe", + "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v6.exe", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "Net2v6", + "appType": "paxton_plugin", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-paxton_plugin-5.0.0.36634-Net2v6.msi", + "path": "windows/nxwitness-paxton_plugin-5.0.0.36634-Net2v6.msi", + "niceName": "Paxton Net2 Pro v6 - plugin installer" + }, + { + "platform": "windows_x64", + "appType": "bundle", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-bundle-5.0.0.36634-windows_x64.exe", + "path": "windows/nxwitness-bundle-5.0.0.36634-windows_x64.exe", + "niceName": "Windows x64 - Client & Server" + }, + { + "platform": "windows_x64", + "appType": "client", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client-5.0.0.36634-windows_x64.exe", + "path": "windows/nxwitness-client-5.0.0.36634-windows_x64.exe", + "niceName": "Windows x64 - Client installer" + }, + { + "platform": "windows_x64", + "appType": "server", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server-5.0.0.36634-windows_x64.exe", + "path": "windows/nxwitness-server-5.0.0.36634-windows_x64.exe", + "niceName": "Windows x64 - Server installer" + }, + { + "platform": "windows_x64", + "appType": "testcamera", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-testcamera-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-testcamera-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - Test camera" + }, + { + "platform": "windows_x64", + "appType": "client_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_update-5.0.0.36634-windows_x64.zip", + "path": "nxwitness-client_update-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - Update package - client" + }, + { + "platform": "windows_x64", + "appType": "server_update", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_update-5.0.0.36634-windows_x64.zip", + "path": "nxwitness-server_update-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - Update package - server" + }, + { + "platform": "windows_x64", + "appType": "conan_refs", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-conan_refs-5.0.0.36634-windows_x64.txt", + "path": "windows/nxwitness-conan_refs-5.0.0.36634-windows_x64.txt", + "niceName": "Windows x64 - conan_refs" + }, + { + "platform": "windows_x64", + "appType": "misc_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-misc_debug-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-misc_debug-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - ~ PDB - Applauncher, traytool & other debug files" + }, + { + "platform": "windows_x64", + "appType": "client_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-client_debug-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-client_debug-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - ~ PDB - client debug files" + }, + { + "platform": "windows_x64", + "appType": "libs_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-libs_debug-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-libs_debug-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - ~ PDB - libraries debug files" + }, + { + "platform": "windows_x64", + "appType": "server_debug", + "beta": false, + "cloudGroup": "prod", + "fileName": "nxwitness-server_debug-5.0.0.36634-windows_x64.zip", + "path": "windows/nxwitness-server_debug-5.0.0.36634-windows_x64.zip", + "niceName": "Windows x64 - ~ PDB - server debug files" + } + ] + } + ], + "releaseUrl": "https://updates.networkoptix.com/default/36634/" } \ No newline at end of file diff --git a/Samples/Packages/DWSpectrum.json b/Samples/Packages/DWSpectrum.json index 656c1620..1bec1555 100644 --- a/Samples/Packages/DWSpectrum.json +++ b/Samples/Packages/DWSpectrum.json @@ -1,155 +1,155 @@ -{ - "version": "5.0.0.35271", - "cloudHost": "dwspectrum.digital-watchdog.com", - "releaseNotesUrl": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", - "description": "Support for Ubuntu 16.04 will be discontinued in the next release version and Systems on Ubuntu 16.04 will be blocked from updating to the next release version.", - "eula": "<!DOCTYPE html>\n<html>\n<head>\n <meta content=\"text/html; charset=utf-8\"/>\n <title>DW Spectrum IPVMS EULA\n\n\n\t

DW SPECTRUM IPVMS END USER LICENSE AGREEMENT

\n

IMPORTANT – PLEASE READ CAREFULLY: This End User License Agreement (the “EULA”) is a legally enforceable contract between You (either an individual or an entity) and Digital Watchdog. (the “Company”) regarding the use of the Company’s software programs accompanying or provided under this Agreement together with the SDK made available by the Company, associated media, corresponding Documentation, technical configurations, and technical data (collectively, the “Software”). “Documentation” means the user’s guide and other documentation (including print and online), if any, provided to you with the Software.

\n

BY CLICKING “I ACCEPT” BUTTON BELOW OR BY DOWNLOADING, INSTALLING OR OTHERWISE USING THE SOFTWARE, YOU CONSENT TO BE LEGALLY BOUND BY ALL THE TERMS AND CONDITIONS OF THE AGREEMENT. IF YOU ARE ENTERING INTO THE AGREEMENT ON BEHALF OF A COMPANY OR OTHER LEGAL ENTITY, YOU REPRESENT THAT YOU HAVE THE AUTHORITY TO BIND SUCH ENTITY AND ITS AFFILIATES TO THE AGREEMENT, IN WHICH CASE THE TERMS \"YOU\" OR \"YOUR\" SHALL REFER TO SUCH ENTITY AND ITS AFFILIATES. IF YOU DO NOT HAVE SUCH AUTHORITY, OR IF YOU DO NOT AGREE WITH ANY OF THE TERMS OF THE AGREEMENT, YOU MUST NOT ACCEPT THE AGREEMENT AND MAY NOT USE THE SOFTWARE.

\n

In addition, certain third party programs or code (“Third Party Software”) are being provided to you under the terms and conditions of a separate end user license accompanying such Third Party Software (“Third Party Software Licenses”), which will be provided to you by the Company as part of the Documentation or may be made available for viewing by the Company at the website or webpage at https://dwspectrum.digital-watchdog.com/content/libraries(the “Third Party Site”). If required by any Third Party Software License, the Companywill make such Third Party Software, and the Company’s modifications to that Third Party Software, available to you on the Third Party Site. The Company may be required or wish to provide you with certain notices or information about Third Party Software or Third Party Software Licenses and you acknowledge that such notices and information may be provided to you by the posting of such notices and information on the Third Party Site. BY INSTALLING, COPYING OR OTHERWISE USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS AGREEMENT AS WELL AS THE TERMS OF THIRD PARTY SOFTWARE LICENSES. IF YOU DO NOT AGREE TO ALL SUCH TERMS, DO NOT INSTALL OR USE THE SOFTWARE.

\n
    \n
  1. License Grant.Subject to the terms and conditions of this Agreement, the Company grants you a non-exclusive, non-transferable, non-sublicensable, limited license to install and use the Software, in object code version only, for your own use subject to the restrictions contained in this Agreement.
  2. \n
  3. Backup Copies.You may make one backup copy of the Software, provided your backup copy is not installed or used on any computer and further provided that all such copies shall bear the original and unmodified copyright, patent and other intellectual property markings that appear on or in the Software. You may not transfer the rights to a backup copy unless you transfer all rights in the Software as provided in this Agreement.
  4. \n
  5. Ownership.You acknowledge that the Software and any copies that you are authorized by the Company to make are the intellectual property of, and are owned by, the Company and its licensors. The structure, sequence and organization of the Software are the valuable trade secrets and confidential information of the Company and its licensors. The Software is protected by copyright, including without limitation by United States copyright law, international treaty provisions and applicable laws in the country in which it is used. You acknowledge that the Company, or its licensors, retain the ownership of all patents, copyrights, trade secrets, trademarks and other intellectual property rights pertaining to the Software. You will take no actions which adversely affect the Company’s intellectual property rights in the Software. This Agreement is a license and not an agreement for sale. No title to, or ownership of, the Software, or any intellectual property rights subsisting therein, is transferred to you.
  6. \n
  7. Restrictions.The Software is licensed solely for your own use. The Company and its licensors reserve all rights not expressly granted to you in this Agreement. Without limiting the generality of the foregoing, you shall not nor shall you permit any other party to: (i) make copies of the Software except as expressly set forth in this Agreement, or (ii) disassemble, decompile or translate any part of the Software, or otherwise attempt to reconstruct or discover the source code of the Software, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding this limitation, or (iii) modify or create derivative works based upon the Software, or (iv) externally distribute, sublicense, resell, encumber or otherwise transfer the Software, or (v) rent, lease, lend, or use the Software, or any part thereof, for timesharing or bureau use, or (vi) allow a third party to copy, access, or use the Software (except as expressly provided in this Agreement), or (vii) alter or remove any copyright, trademark or other proprietary notice which may appear on the Software, (viii) take any action that would cause the Software to be placed in the public domain or become subject to open source license agreement, or (ix) use the Software in any manner that violates any statute, law, rule, regulation, directive, guideline, bylaw whether presently in force or may be implemented by federal, state or local authorities.
  8. \n
  9. Paid Features.The Software is made available to you at no charge or fees. However, the Company may offer certain premium features to you upon payment of a license fees determined by the Company in its sole discretion (“Paid Features”). In order to use Paid Features, you must agree to pay the license fees plus any applicable taxes through one of the payment methods accepted by the Company. Upon receipt of the license fees, the Company will either provide you a license key to activate the Paid Features or allow you to download the Software with the Paid Features. Activation of Paid Features may also be regulated through the use of transferrable license keys, and such license keys may only be transferred within your organization subject to the limitations listed in your purchase order or other documentation describing the Software and/or license rights obtained in the Software.You expressly acknowledge and agree that this Agreement will govern your use of the Paid Features.
  10. \n
  11. Updates.Except as otherwise expressly provided herein, this Agreement will govern any updates and upgrades to the Software that may be provided to you in accordance with the Company’s then-current maintenance and support policies, unless such updates and upgrades are provided under a separate license agreement. You may use such updates or upgrades only in conjunction with your then-existing the Software licensed under this Agreement. The Software and all updates and upgrades are licensed as a single product and such updates and upgrades may not be separated from the Software to exceed the scope of your original license. Any other software that may be provided with the Software that is associated with a separate end-user license agreement is licensed to you under the terms of that separate license agreement.
  12. \n
  13. No Support.Except as expressly required by applicable law, nothing in this Agreement entitles you to any support, maintenance or new versions of the Software. You may contact the Company to determine the availability of support, maintenance, and new versions, and the fees terms and conditions that would apply.
  14. \n
  15. Disclaimer of Warranties.THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OR REPRESENTATION OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. NEITHER THE COMPANY NOR ITS LICENSORS REPRESENT OR WARRANT THAT THE SOFTWARE WILL SATISFY YOUR REQUIREMENTS OR THAT IT IS WITHOUT DEFECT OR ERROR OR THAT THE OPERATION THEREOF WILL BE UNINTERRUPTED OR ERROR FREE. The Company does not warrant that functions contained in the Software will meet your requirements or that the operation of the Software will be error-free or uninterrupted. The Company is not responsible for problems caused by changes in, or modifications to, the operating characteristics of any computer hardware or operating system for which the Software or any upgrade or update is procured, nor is the Company responsible for problems which occur as a result of the use of the Software in conjunction with software of third parties or with hardware, which does not meet the Company’s specific requirements. Some jurisdictions do not allow certain disclaimers or limitations of warranties, so some of these may not apply to you.
  16. \n
  17. Limitation of Liability.TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW EVEN IF A REMEDY FAILS ITS ESSENTIAL PURPOSE, IN NO EVENT SHALL THE COMPANY OR ITS LICENSORS BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE OR THE PROVISION OF OR FAILURE TO PROVIDE SUPPORT SERVICES, EVEN IF THE COMPANY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN ANY CASE, THE COMPANY’S ENTIRE LIABILITY TO YOU OR ANY OTHER PARTY FOR LOSS OR DAMAGE RESULTING FROM ANY CLAIMS, DEMANDS OR ACTIONS ARISING OUT OF THIS AGREEMENT SHALL NOT EXCEED THE FEE ACTUALLY PAID BY YOU TO THE COMPANY FOR THE SOFTWARE, IF ANY, NOTWITHSTANDING ANY FAILURE OF ESSENTIAL PURPOSE OF ANY LIMITED REMEDY. Because some states and jurisdictions do not allow the exclusion or limitation of liability, the above limitation may not apply to you. In such states and jurisdictions, the Company’s liability shall be limited to the greatest extent permitted by law.
  18. \n
  19. Sole Remedy and Allocation of Risk.YOUR SOLE AND EXCLUSIVE REMEDY AND THE COMPANY’S SOLE AND EXCLUSIVE LIABILITY ARE SET FORTH IN THIS AGREEMENT. This Agreement defines a mutually agreed-upon allocation of risk.
  20. \n
  21. Third Party Software Licenses. Third Party Software included with the Software is licensed under the terms of Third Party Software Licenses. You understand and agree that, although provided to you by the Company with the Software, your use of Third Party Software shall be and is governed by such Third Party Software Licenses. You understand and acknowledge that Third Party Software is being provided “AS IS” WITHOUT ANY WARRANTY, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. If there is a conflict between this Agreement and any Third Party Software Licensewith respect to Third Party Software, the provisions of such Third Party Software Licenseshall prevail.
  22. \n
  23. Export Restrictions.You agree to comply with all export and re-export restrictions and regulations (“Export Restrictions”) imposed by the United States or any other country in which you conduct business. Without limiting the generality of the foregoing, and regardless of any disclosure made by you to the Company regarding an ultimate destination of the Software, you represent and warrant that you understand the U.S. law currently prohibits the export or re-export, directly or indirectly (including via remote access) of U.S. origin products and technology to certain proscribed countries (including Cuba, Iran, Sudan, North Korea and Syria and/or any other country that may become subject to an embargo by the United States), entities, organizations and individuals, without prior authorization from the U.S. Government. You will not commit any act or omission that will result in a breach of any such Export Restrictions. Your breach of this clause shall constitute cause for immediate termination of this Agreement.
  24. \n
  25. High Risk Activities.The Software is not designed, manufactured or intended for use or resale as on-line control equipment in hazardous environments requiring fail-safe performance, such as in the operation of nuclear facilities, aircraft navigation or communication systems, air traffic control, direct life support machines, or weapons systems, in which the failure of the Software could lead directly to death, personal injury, or severe physical or environmental damage (“High Risk Activities”). The Company specifically disclaims any express or implied warranty of fitness for High Risk Activities.
  26. \n
  27. Consent to Collect Data.You consent Company to collect and use technical data and information from the installed application anonymously. The collected information include but not limited to the version of the Software, Software configuration, devices added or connected to the Software, total session time, Internet/network usage, and hardware specification. The information is gathered to provision the Software usage, updates, devices and configuration so developers can enhance and improve the Software.
  28. \n
\n

The data collection can be disabled from the Software if you do not want the data to be sent.

\n
    \n
  1. Termination.The licenses granted herein shall automatically terminate without notice if you fail to comply with any material provision of this Agreement. In such event, you must immediately uninstall and return the Software, including all Documentation and original media, to the Company. This shall not limit or affect any remedy available to the Company for your breach of this Agreement.
  2. \n
  3. Miscellaneous.
  4. \n
\n

(a) If you are a unit or agency of the United States Government, the following applies: The Software is provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the Government is subject to restrictions as set forth in Subparagraphs (a) through (d) of the Commercial Computer-Restricted Rights clause at FAR 52.227-19 when applicable, or in Subparagraph 252.227-7013 (c)(1)(ii) of the Rights in Technical Data and Computer Software at DFARS, and in similar clauses in the NASA FAR Supplement. Contractor/manufacturer is Digital Watchdog.

\n

(b) This Agreement shall be governed by the laws of the State of California, exclusive of its choice of law rules. Any dispute between you and the Company regarding this Agreement will be subject to the exclusive jurisdiction of the state and federal courts sitting in the County of Los Angeles in the State of California. In any such action, the prevailing party will be entitled to costs and attorneys’ fees.

\n

(c) You may not assign, sublicense, or transfer this Agreement, the Software, or any rights or obligations hereunder without prior written consent of the Company. Any such attempted assignment, sublicense, or transfer will be null and void. The Company may terminate this Agreement in the event of any such attempted assignment, sublicense, or transfer.

\n

(d) This Agreement is in the English language only, and will only be provided in the English language, which language will be controlling in all respects, and all versions hereof in any other language will not be binding on the parties hereto. All communications and notices to be made or given pursuant to this Agreement must be in the English language.

\n

(e) The Agreement constitutes the final and complete understanding between you and the Company with respect to the subject matter of this Agreement. Any modifications or waivers of this Agreement must be in writing and signed by both parties hereto.

\n

(f) If any provision in this Agreement is invalid or unenforceable or contrary to applicable law, such provision shall be construed, limited, or altered, as necessary, to eliminate the invalidity or unenforceability or the conflict with applicable law, and all other provisions of this Agreement shall remain in effect.

\n

(g) Failure or delay on the part of the Company to exercise any right, power, privilege, or remedy will not constitute a waiver of, or bar the later exercise of, that or any other right, power, privilege, or remedy of the Company.

\n

(h) No rule of strict construction shall apply against or in favor of either party in the construction and interpretation of this Agreement.

\n\n\n", - "eulaVersion": 1, - "packages": [ - { - "component": "server", - "platform": "linux_x64", - "file": "dwspectrum-server_update-5.0.0.35271-linux_x64.zip", - "size": 88614643, - "md5": "e027ae6b5f4420ac77dfd76f4e502f99", - "signature": "DF54oM/oYcBZ6h6gamUt/QfGoV+yxA/yfkrDZyqqhhXdg3uALI5kRYQy7A32EvmThB1DQ2pi/5Y7s4EHAg4kJWOgPZiwaO1cQ8++gai4YVes2duzLFqkUHCFAvoD61QQSps+Wbtq/agcH9nR0RrnhvYdvG0kRkcuOrESWN3XkuC0g+LxczUpuDXYtZ3SMjuoQ60UTkrQDcMsb/kcfFEzyIL+aRlV2u1tBbdYjADP1opQjPAtbVjrfzlTXTz75Hn1HldtXdO8vByIm9ljhKFETs0FFjXj6YUn0TdzLffw0Bs/IFyrZ7KcvQW7M62rKN8z+1u0KvwQKatrh8+ZFCEZAFx8MH+gUy5sTcyRrvInrmvDnL1r7WVmXv7p3gWFLSewtMDlcBOK7X+rW4+c5pk5ltQWM5G2V50lYsVOOPjjFvoDSpkdAOksXRWg06unS3tU2jDRlFEEE46HLLueuFH7a2fTG/IX8LcRE/Pcfn0kDHEzjB3lZnwDahGwH42CynzaZUXRW6KH73MwcwYDPO/fjBzB7YrXhBicMCHxASPjDMD3ml/7lbOdqGPeeknbNhnfVkgze+S0nr9OIaTQ2CcVfIHRmmjtbMG2cIo7X+3wgdUaSfSBOGoZA/8yIyAJehiz9XlpjOEpFD2P+ndGQsIsIIExXhNfOxUmdjsFLye31IA=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "16.04" - } - ] - }, - { - "component": "server", - "platform": "windows_x64", - "file": "dwspectrum-server_update-5.0.0.35271-windows_x64.zip", - "size": 97987920, - "md5": "c809fdd6cd162e368a8593b211c35e5f", - "signature": "AmCSSckelnzgMzganq9H/m8Ekwg+fmbVGByxGDXIvQljFWw2D/B17nnEVdfH8vE5NVPO4Co7qW3xdJMSGJrJCaHEf5ijs5SJe3LEGDAiNjZEsGH9MFMJNnOPyS3W9PysLDL/5SFSHYcXkgeug+erlnuC8MAkVtb/GDq9s/8nqIY/KWKQ3hNn67uq5lJe+8i4A9GK5jailev355JnGvGrs3+9Hu47+dSWMl2dYq2IYsNsO2bDbknaUpIctkTxhQ/RDBuyazOHT/67P5IwedTemzWvo5/oJqPaU7khx82qKqEgifmZXLD8eZX3aPwTq3NtXbdBmrqcHEmdpG0s+pVhWHYUUjlPpyQ0p+VqeJKqC1PvecGdZpMVkj7Ix8/FlJbu5TtO+0SY6bg/h1hgQHD2Vtf8PqapeyrKigprPONz6JVYIvmkOTLIrfaEOrFhsIF1cNzYXxuQI1T/U/q6Okisr4P88Wkcd/ZDAbj3Z95fyRxFEozsWOa2s6WA27+0NRKoNR08GzhR0LIc1HqKOL9pwgNAwcDnqPca0R35tqDDGOUj+Czt5mE6dgnTSG4XNPr0G1tsU9Xt5+4dhYRIfkqFYyML42kq9SVfZLCL3GbLbL1Lpo09RKbUwu0zJllCk8itRb6FASevcZPpDNOYhWo08ijABEiYIdhUcOdKhyt9IWw=", - "variants": [] - }, - { - "component": "server", - "platform": "edge1", - "file": "dwspectrum-server_update-5.0.0.35271-edge1.zip", - "size": 85437501, - "md5": "018f57ac6160f849c7eaf51ceea7d258", - "signature": "mkWJRgn+t1e+5811FaPMSSokpwRrtc4L0UcIBDvdpVBTEzhtBcl7FzJBJHP9ljhHW71eEnev4j269C/S4FNn59m01+WsGdiiqd29ReH7zw8mL+YUjjitlzpCNSnSh3fwcvAQ5piLyNcMgdEXFkoZE/A8T8BzKgw6dSfkLhM/5LFftdB8BS1QG4poA83fl1XJqCCDzXt0mgDcCQGwj6s9fEnCe56eB8dDFwoA8M4LpAdojdbKuvxXSi/VRa7CYwvyvwQQaeqSJ44PLD/NrNjL68vl8Mm2HW95goIovE1MpycUCoB7Hb9anfxnP5diWsA/537FhWUSABEjml4L2j6ncGZC3ZatXBpC4Jr2ewPuUBaBJQMFY64fYMPy5jcL2GdiX4Kjl8S0XDUDHmywxl7eFTjuB9LPOjLUWWHd9D3w8vi40OefwIbHfGpE2gNKCb6pkvmNgRvwn6a47hNJ1iTNGuOigq0b/pug/8WxZDSdHkMuq3M61l0MPeucydD3AGWbpMv+teceYTd3DH/gYksM8++jRccErGPH5LOpzlHwZnHorHfyp1/iVoKVL77I5aOzU2vgpiv+sJNHvEj2PIPg7pyUjAZbHvFMwusfm2w1fCGla9Df/lmiDAEogVG4qqBJuKn8lYe1/xZwYH/L4oQ3+zSHc/+JeXwmo2ynaekZzYk=", - "variants": [] - }, - { - "component": "server", - "platform": "linux_arm64", - "file": "dwspectrum-server_update-5.0.0.35271-linux_arm64.zip", - "size": 143427373, - "md5": "589df20cab8d54a1eb1c909cfd703f75", - "signature": "RuCn/6jOR5z5wf6JEspzPB1HyDMTYauUKxqBkIrxbS8YEO8MVpd2jrRd5hkd0u58fZ/9CZIXHFplc9887K3MPkjgn1YoDSO6lv5sieLxO8lM7LYtaFQs6H8/YIv7QZnooyOv1cgLLkRvb/LcLmEkbIBPHKLQ8ChDUJD6bmFyolxcGRjFfiAyFRzEsqBIwiK/gr6JmyC5DBqFbz5MyBafMt/BwWXrH4e0e9zgDSulBJC2FLoUKnewEtCN6TUDD248d2hGrUz4Q8JXIzcGjSxt0jwnc3c7TNpkVxfuy0e5Ob7KCTWnjX/LNzvD6bimMBWzxPQXIlvn1OmLsv4fMm5Qzz4ZX0FWTOxZsZO8B5VWfVBYJaGtwPhXOZhNt3IsS9ffTKG8K9C0taTQTi8JQVbZHgwjzRY87fuC9NxGYXYJ3HXriaQ/VB8s6mvy7GOee+cqux145B09pkndJIy46QF0VTypmDZC6G8fCCY8tT9tOtJcksgizh0QchwNNik+KInx1wgxds4vTN8ld9CmtWoJEfB0ydmBGHL0/vZksj+o9+ds8nD7gfTfdo+4RuMiDmzQX5sOehg8u1fAHuGVUOydqOGzgHa9WPKdtQ3CpzipKLnu4BRJRqpE5LJ0KXAM1yTxmskINIzODrlZPRkeCG63uarbJSlQlhEHajLGONyRJNk=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "16.04" - } - ] - }, - { - "component": "server", - "platform": "linux_arm32", - "file": "dwspectrum-server_update-5.0.0.35271-linux_arm32.zip", - "size": 169064419, - "md5": "39e11cb1c489b0993380dd8b2a2e99a4", - "signature": "Kd2gdCR7oFSNLfrqb8XZPf0NmuNZ6LpHznAtNoDL6DqpnWBXsZ2YdqtAvYmpWX0EoqJZ7TRoyjyEKiu06kalgOXE+xFyEhAYTaQp50MIpkq3kutZ8l5rnkum99WvCW0NnHPBIF1OlO7xBFEQMWWDSSmooVaR9EqxhajIsYBj+8mzBTvOnNCn5cDgv1/4V+GCSIjHAeDiLE3CB0hr8yprzKKhKfdZQMTYfHziVspjDLTgeSx07cehEhYqn9uJb35QZN4ily4e3htbXPPLbPNMP2/Sn97mJYFRm7tMs8xzUImUE5gGV5Fc6SFmiYK/otpYtxUyuexRTZU5w7dHQXuRksIE3b8BQQ8CRFhRd0zN534hm9whNRI0IYnBPOG2oaEA1mWsTEvAj+9Gzp8WsIKCHXRE1Ybicqa8D7tdO781sBgrW5ltiEZEmZWOo/p5Z/boC87UTib1s3aQG10lccW8srL4skirydboipFPE3QYAjcnOc3eocRN4A9LHtDbgCTmME2cRAAAvIYDCwiVSfnbuWWrkOSGDXv+A06OwCB1l+OS2qUecaHg+QoiFYGXffBStNCU0RyVg8QWbeOjxBD2xcpFquypyo6+jxXO/ZPZ5AUXepBB1iS5iIWpqpGc0RHtlDQSRdse1xFVshc6CCOvk1H0EO9RqDq1oxmv+PATo7g=", - "variants": [ - { - "name": "debian", - "minimumVersion": "9" - }, - { - "name": "ubuntu", - "minimumVersion": "16.04" - }, - { - "name": "raspbian", - "minimumVersion": "9" - } - ] - }, - { - "component": "client", - "platform": "linux_x64", - "file": "dwspectrum-client_update-5.0.0.35271-linux_x64.zip", - "size": 268837878, - "md5": "265a473af3a8bda35984e924d3783e05", - "signature": "MLGEe7AcSZbch2+Bvh2RUhdydCbuFXOnRjpm9rJWsp/eOYVq2ZXsyOwoEV6sZexCAxfQS3LZg8xPnGTklJE6V1FP2Qc7GnxW/jPAhRKoWeP931+m//ZRfvkg4adLTLId9fVgvtX95btqrqdqLUUBSoWHAZoO/XrliHh0JsnSACbF1umv1l6byhxJAEHVKWEFzc7aTX/ibsfnZljDI1qLONtRWIOE8Gg+gct65B0uq/FLr8FcLfocD1v4nkRQkAtBB4mk1TjOsIyKMweOhnuvwNE5XUnx6IjBUDuxMFYAkyRfsZ1IbydMUzwcZHUnCAwbo6XbloJsu1yH55jYwnJNlHGJnPC+xCyThJiWvXfypgp/1WqS35yMFH2e3TSZxKK7SUiF7f8Hxkjc7iYM/Ba71KvsoU07Q+nE7if1EEUOiHqeUCrwYZg0R/cSUiMfqfx0A3OuuVeQTQKTPZ35VI/Wgry7I0buV9kpugHNshQ2U2ZTZYG8ipMC0HdoXaS//5Tfh+iK0xqhdTjzWjZeNwDPs9F+ap4ICzkWK27qgtszNM0XgFczkGE6g+I/V5oJH3f32qn+fi2YGJOLGer2samoy8lnLzNamLszUSuUojLvlEIAdR5cjsErwln/srjIFQb1Wuy8+sMqaV2DYHSexuM7oQKK5LMaNdlACZ89O+XXZzs=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "16.04" - } - ] - }, - { - "component": "client", - "platform": "windows_x64", - "file": "dwspectrum-client_update-5.0.0.35271-windows_x64.zip", - "size": 153542461, - "md5": "2ce20c6bb040a39c2c76d63b5e072c08", - "signature": "GNdBfjjZKscX1+wya6AgEkCTfM9fg0H/T+MW6QtAINhGI+iaAyJfAKF9/6KZ2KiotkUud9mnmr3gi2Bkq/GdxpTbv6EOidkzgcvbSJTEATf6CDI7F1aJH19wQFzor05Y31y9TyMJWYHIO8Yue9DH5xoVUAZptbOxHyil3gXuQVk8LAZZWd6pNoA8ABQS5V8WBf2zzt4cSvxMlc1aKf2PPiimyk4Sklq/ZEt0N5TqWkxTnReA/32IoZGAbYFeaWAhpuZhP+BixYuDEdLoR/3WZ/SRPZei4hg7/g4gUPB3+isIpiE+3LWY3O/BkBQm1ZQuayCbeJwHiUGlUfQ+gQwVbPJAEUbJ6TaOYgUeIao+pBXPFa0LQtG/ISJcbsG0xKbGcOox1NRT81WGMynjrfxLGiNPGkU+YurDF3CN8Cctmd1wkqbY32g4XWw4BsaHwTCRKRdJdaoowbYBSsjiCYQ/X/FJChcVFcKcQNhLVc/aVSdy9PTp4kfWWpAdPzaXI7TgSSEWBGatKkE0av6xshWCMZT0lbk1aQr8iyq/LjamUJwPVDsxtTas4ppfJDV5VgqVfvEazf4TyMlbHyi4Db776caZWFSQto6szh2JTMIfq35oaJTL+nCXnD8AkTS3jot/qLAUFBY4hCV0FM2W5U7G4+kM4dotVxbTdaK4o1UhnwI=", - "variants": [] - }, - { - "component": "client", - "platform": "macos_x64", - "file": "dwspectrum-client_update-5.0.0.35271-macos_x64.zip", - "size": 158000043, - "md5": "4bb2e9498267f51fc7057c2695bc417d", - "signature": "TCxNIKAlgbVKXXUessdjx0manmIFgYuKPjbnLMhe05MQ1Gg7nhUtfthQipJ8R+ewiy487L1KxSfu5uUCBAu7zcMnckjtM2t/owYv7PLb00HnqLzXfY6ZhfGQ9gVHJbHhANZDtsRlzAdFX+263+l3O3Uh/5Ci7rkIsjW7KmiIlC5yePipK3Pnp7R9VR8G4Bp2ZVlPoseRD4ovg+HGN4xABCZk7A784KE66/nEnXGUuUFofij6O30+YZYvgk4GTzQ5CX4iwko2y/eXFSUm7ftCN+FV55YQuShwgocV/uTmVD2git+mR4cBELBIfVOkTkwKiwMij+h3DhJPZnzvUC3qcQbiejRN+6sVA48z6+RcI0nmG+HVvkOesceqNQ+UeG7kRFsgBQXM9f2twy2f5ui8u7eh4/YXenCvzk7tOzjHeAcTjuorWJsL5f7dd4bl7EZVwo7itS0jmKitSmQvaUBxEAfMEjRTpgmZnk0PH3wHT75WyE9Bu9mVG0rFA5E1HUcOmQg5qyzv9C9mPohUSMfZSRbBNcrBr2kcikzdZS3bj/TsGhP6c10SFts5M0npdtNZiBMIrTInRKaPVfihG3wYaCNAjJO0yR7PpXet5qkAP0bUWh0E4yEWdm3vKyFrLMfkui7Xr8r108n4TlZflYOmY6hK7+o9rIJU+LqvFZyo8hc=", - "variants": [] - }, - { - "component": "client", - "platform": "macos", - "file": "dwspectrum-client_update-5.0.0.35271-macos.zip", - "size": 158000038, - "md5": "c3b126cb6d4558f5c38b52e423269dd0", - "signature": "bHJUbQtwFm/PkkFpAAbIjnw/iWPDZX8GMkmUmJSXB8VGkxy5kITi9iV9P1iHc2GhBM7hXV2ihYuEhDoRfI2p79oe/a4Aa1lT/iIS7x1EA6hQ3VeD5DwWVfP3OvDFv98h0F/6QAvxbCDAdF12YTMLWn/DBQ1kgL4dZz0wgkxrrvDwSuaRp2Z2RGJGnX85A4CyNrgujWYRMtAhZ9TusVqF5LSVE2BZoBqGa/p26v3SbKy7b5KpHu94xyKtFvL0JTSzL0LBZj3BfupSXJMuGKdaLnlbZlN5L+aoEO1+uKuht7qMVKI3fiXVlAoH0ZjadBAcyb+NEsYnz970TGq2tUm0RXpQkGdF5f/QW8xE2sEDZEDCER66jRceIqig6e2OChECYRJ4m8s93hwcNm0lc9IfqkfsN/V2c/S4D9pOCx0L2fwGpEVawqTAFAwJnUqiOgDwWut3iTXair22ani67d6wreulqQyEBoh9wkNph21toaFq3FX9+ChRGXW8oe63m4WdgZD/n9zdlgyhbg03HbO8CUJI5OyiMdrQxhkyyPlz+7r3y2RXL+GrnKlX3mvXtDvC4cFQ4Fz+9mKZ72A6yX/NN72C+ca73Or4XWOPrFwrR0z98oWLozoAaFiexYz2+gIibNW4uPoVUk9f62Zdb7mMJsnmTcMouRjAun9xOF1SAig=", - "variants": [] - }, - { - "component": "client", - "platform": "macos_arm64", - "file": "dwspectrum-client_update-5.0.0.35271-macos_arm64.zip", - "size": 144560236, - "md5": "c2cc87a5915d02d9b74354838236a083", - "signature": "zBV4SXYDfpq9+veBn3wV4NyzSQtt6b7h0kIKgNOyUYXxilQEnjqxkAoqEe6YZPBdifQrJT7OSzLUD11zxWg2tHpsQB/jsX2QaItZO7AWYaMxRotbZ25yzcmCW/Tx+cHDyG0xNP5R6tE/3MQR+8FepnoLP+DgAFFyrCl+U3kuW74H6hYSS3RlsTVoz4/41wxc/jkleYKEQHO/LRd2j3+T+DQTGAPaBBTeRPR4GcOpdTzSMtNm6QA6bZQmj3Wh5wuKQc8fw5k5Ixjcc+TAj7gYMLBvf4jWgD+4soYqv/XftVbr9FL+WwBTYjKTjJA89n/MHXuYTV4rsU8xYGMiBloWZQwO/4kVLEJcbSKy7NpEP61qcgGj19TUK/aEhD3a12sgcaUzvP3sOc8c65yy66CkqKZWIUcPiGYjvweltvWBmpSBG2dH92GDBO9tU9Ivoi9PPrnoxxN2YcVghDZVmirXi+WlIJy9NDH2xONN87im4bsMZB1V4dMrbhQSTXIq0rKzVKWU37JrgXml1qsjl2zqUt4xl30DxPwYoH9AIxk++uhMlGmH16kgVywlOBUjyV6/NEc1MM0ac3zwqTYgy6edpGufLRjttg6oKLMZ6Xc55XMvILkzU7V3AQqW1L0qJJSHuwUWhERhQzLE7sIKgMQBNFtte0MxjpseEJ0xxg/4wSY=", - "variants": [] - }, - { - "component": "client", - "platform": "linux_arm64", - "file": "dwspectrum-client_update-5.0.0.35271-linux_arm64.zip", - "size": 273433878, - "md5": "e6b7170ff2557c6b34f76761d16a6794", - "signature": "m1KZMp1DTuD7vwKhnUzMlL827EhQHYs1Y5Hmg/ufvfw0fgCCES+b2j4XWUGv2Y1L+MZWVc5lhrydcTCooVL57pESub8SSaKd8BIqjrjv4ZitOWzSPlN/kaOylV84NM8Zf6PTPl+/hxkjwFGqlI3RPp6nw1AmUIJOJLNM1YxfnRE8COBCKqfey5kWLclGnqGvfRcyQ8BSAiL4o1rnewm0773faeX7JKoQTJ5xwPxqVyinIjZxzXZK5DEuC9QHj+b/uwiATAzhcObij3xmq5BR8xTg3EboaKHeAMu8zDQhTHLhdnVwyeRZtLo961GbuavaMYJ+nxEd6Nb8/WiM8dUUAL0eVo6ngHJFxJ9ChtFe6Bz55cZNsCxZdkhSzmrVk3uH9puWJ98HBQYlCe2GEx/9W+Lm10qOhpQ1B3BdCYqK6JcMvtnfZMYtvYlaJ2UuDm+tFWCJhidjVbGDRJ9DQ/q+BoiknslPndfYZvARtOP8/CxB9gUhD820yAzjKhjmx2NeiO/TEg37w8oWjdaaXegmWq2fmq3Hue13RQSh52pgwLqHEeikunxo36TgKe0aY+3lpANzSgzIzyO7ngbiJgTgsd9kCnXRuyLfGVmXGZcpFeK5ptIAU5bU5nmcQm628JH/Cjeln+WhQFFThM0w3fP0DrQ84KbuCYvp5keuue8j+D8=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "16.04" - } - ] - }, - { - "component": "server", - "platform": "nx1", - "file": "", - "size": 0, - "md5": "", - "variants": [ - { - "name": "", - "minimumVersion": "3000" - } - ] - } - ] +{ + "version": "5.0.0.35271", + "cloudHost": "dwspectrum.digital-watchdog.com", + "releaseNotesUrl": "https://digital-watchdog.com/DWSpectrum-Releasenote/DWSpectrum.html", + "description": "Support for Ubuntu 16.04 will be discontinued in the next release version and Systems on Ubuntu 16.04 will be blocked from updating to the next release version.", + "eula": "\n\n\n \n DW Spectrum IPVMS EULA\n\n\n\t

DW SPECTRUM IPVMS END USER LICENSE AGREEMENT

\n

IMPORTANT – PLEASE READ CAREFULLY: This End User License Agreement (the “EULA”) is a legally enforceable contract between You (either an individual or an entity) and Digital Watchdog. (the “Company”) regarding the use of the Company’s software programs accompanying or provided under this Agreement together with the SDK made available by the Company, associated media, corresponding Documentation, technical configurations, and technical data (collectively, the “Software”). “Documentation” means the user’s guide and other documentation (including print and online), if any, provided to you with the Software.

\n

BY CLICKING “I ACCEPT” BUTTON BELOW OR BY DOWNLOADING, INSTALLING OR OTHERWISE USING THE SOFTWARE, YOU CONSENT TO BE LEGALLY BOUND BY ALL THE TERMS AND CONDITIONS OF THE AGREEMENT. IF YOU ARE ENTERING INTO THE AGREEMENT ON BEHALF OF A COMPANY OR OTHER LEGAL ENTITY, YOU REPRESENT THAT YOU HAVE THE AUTHORITY TO BIND SUCH ENTITY AND ITS AFFILIATES TO THE AGREEMENT, IN WHICH CASE THE TERMS \"YOU\" OR \"YOUR\" SHALL REFER TO SUCH ENTITY AND ITS AFFILIATES. IF YOU DO NOT HAVE SUCH AUTHORITY, OR IF YOU DO NOT AGREE WITH ANY OF THE TERMS OF THE AGREEMENT, YOU MUST NOT ACCEPT THE AGREEMENT AND MAY NOT USE THE SOFTWARE.

\n

In addition, certain third party programs or code (“Third Party Software”) are being provided to you under the terms and conditions of a separate end user license accompanying such Third Party Software (“Third Party Software Licenses”), which will be provided to you by the Company as part of the Documentation or may be made available for viewing by the Company at the website or webpage at https://dwspectrum.digital-watchdog.com/content/libraries(the “Third Party Site”). If required by any Third Party Software License, the Companywill make such Third Party Software, and the Company’s modifications to that Third Party Software, available to you on the Third Party Site. The Company may be required or wish to provide you with certain notices or information about Third Party Software or Third Party Software Licenses and you acknowledge that such notices and information may be provided to you by the posting of such notices and information on the Third Party Site. BY INSTALLING, COPYING OR OTHERWISE USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS AGREEMENT AS WELL AS THE TERMS OF THIRD PARTY SOFTWARE LICENSES. IF YOU DO NOT AGREE TO ALL SUCH TERMS, DO NOT INSTALL OR USE THE SOFTWARE.

\n
    \n
  1. License Grant.Subject to the terms and conditions of this Agreement, the Company grants you a non-exclusive, non-transferable, non-sublicensable, limited license to install and use the Software, in object code version only, for your own use subject to the restrictions contained in this Agreement.
  2. \n
  3. Backup Copies.You may make one backup copy of the Software, provided your backup copy is not installed or used on any computer and further provided that all such copies shall bear the original and unmodified copyright, patent and other intellectual property markings that appear on or in the Software. You may not transfer the rights to a backup copy unless you transfer all rights in the Software as provided in this Agreement.
  4. \n
  5. Ownership.You acknowledge that the Software and any copies that you are authorized by the Company to make are the intellectual property of, and are owned by, the Company and its licensors. The structure, sequence and organization of the Software are the valuable trade secrets and confidential information of the Company and its licensors. The Software is protected by copyright, including without limitation by United States copyright law, international treaty provisions and applicable laws in the country in which it is used. You acknowledge that the Company, or its licensors, retain the ownership of all patents, copyrights, trade secrets, trademarks and other intellectual property rights pertaining to the Software. You will take no actions which adversely affect the Company’s intellectual property rights in the Software. This Agreement is a license and not an agreement for sale. No title to, or ownership of, the Software, or any intellectual property rights subsisting therein, is transferred to you.
  6. \n
  7. Restrictions.The Software is licensed solely for your own use. The Company and its licensors reserve all rights not expressly granted to you in this Agreement. Without limiting the generality of the foregoing, you shall not nor shall you permit any other party to: (i) make copies of the Software except as expressly set forth in this Agreement, or (ii) disassemble, decompile or translate any part of the Software, or otherwise attempt to reconstruct or discover the source code of the Software, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding this limitation, or (iii) modify or create derivative works based upon the Software, or (iv) externally distribute, sublicense, resell, encumber or otherwise transfer the Software, or (v) rent, lease, lend, or use the Software, or any part thereof, for timesharing or bureau use, or (vi) allow a third party to copy, access, or use the Software (except as expressly provided in this Agreement), or (vii) alter or remove any copyright, trademark or other proprietary notice which may appear on the Software, (viii) take any action that would cause the Software to be placed in the public domain or become subject to open source license agreement, or (ix) use the Software in any manner that violates any statute, law, rule, regulation, directive, guideline, bylaw whether presently in force or may be implemented by federal, state or local authorities.
  8. \n
  9. Paid Features.The Software is made available to you at no charge or fees. However, the Company may offer certain premium features to you upon payment of a license fees determined by the Company in its sole discretion (“Paid Features”). In order to use Paid Features, you must agree to pay the license fees plus any applicable taxes through one of the payment methods accepted by the Company. Upon receipt of the license fees, the Company will either provide you a license key to activate the Paid Features or allow you to download the Software with the Paid Features. Activation of Paid Features may also be regulated through the use of transferrable license keys, and such license keys may only be transferred within your organization subject to the limitations listed in your purchase order or other documentation describing the Software and/or license rights obtained in the Software.You expressly acknowledge and agree that this Agreement will govern your use of the Paid Features.
  10. \n
  11. Updates.Except as otherwise expressly provided herein, this Agreement will govern any updates and upgrades to the Software that may be provided to you in accordance with the Company’s then-current maintenance and support policies, unless such updates and upgrades are provided under a separate license agreement. You may use such updates or upgrades only in conjunction with your then-existing the Software licensed under this Agreement. The Software and all updates and upgrades are licensed as a single product and such updates and upgrades may not be separated from the Software to exceed the scope of your original license. Any other software that may be provided with the Software that is associated with a separate end-user license agreement is licensed to you under the terms of that separate license agreement.
  12. \n
  13. No Support.Except as expressly required by applicable law, nothing in this Agreement entitles you to any support, maintenance or new versions of the Software. You may contact the Company to determine the availability of support, maintenance, and new versions, and the fees terms and conditions that would apply.
  14. \n
  15. Disclaimer of Warranties.THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OR REPRESENTATION OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. NEITHER THE COMPANY NOR ITS LICENSORS REPRESENT OR WARRANT THAT THE SOFTWARE WILL SATISFY YOUR REQUIREMENTS OR THAT IT IS WITHOUT DEFECT OR ERROR OR THAT THE OPERATION THEREOF WILL BE UNINTERRUPTED OR ERROR FREE. The Company does not warrant that functions contained in the Software will meet your requirements or that the operation of the Software will be error-free or uninterrupted. The Company is not responsible for problems caused by changes in, or modifications to, the operating characteristics of any computer hardware or operating system for which the Software or any upgrade or update is procured, nor is the Company responsible for problems which occur as a result of the use of the Software in conjunction with software of third parties or with hardware, which does not meet the Company’s specific requirements. Some jurisdictions do not allow certain disclaimers or limitations of warranties, so some of these may not apply to you.
  16. \n
  17. Limitation of Liability.TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW EVEN IF A REMEDY FAILS ITS ESSENTIAL PURPOSE, IN NO EVENT SHALL THE COMPANY OR ITS LICENSORS BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE OR THE PROVISION OF OR FAILURE TO PROVIDE SUPPORT SERVICES, EVEN IF THE COMPANY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN ANY CASE, THE COMPANY’S ENTIRE LIABILITY TO YOU OR ANY OTHER PARTY FOR LOSS OR DAMAGE RESULTING FROM ANY CLAIMS, DEMANDS OR ACTIONS ARISING OUT OF THIS AGREEMENT SHALL NOT EXCEED THE FEE ACTUALLY PAID BY YOU TO THE COMPANY FOR THE SOFTWARE, IF ANY, NOTWITHSTANDING ANY FAILURE OF ESSENTIAL PURPOSE OF ANY LIMITED REMEDY. Because some states and jurisdictions do not allow the exclusion or limitation of liability, the above limitation may not apply to you. In such states and jurisdictions, the Company’s liability shall be limited to the greatest extent permitted by law.
  18. \n
  19. Sole Remedy and Allocation of Risk.YOUR SOLE AND EXCLUSIVE REMEDY AND THE COMPANY’S SOLE AND EXCLUSIVE LIABILITY ARE SET FORTH IN THIS AGREEMENT. This Agreement defines a mutually agreed-upon allocation of risk.
  20. \n
  21. Third Party Software Licenses. Third Party Software included with the Software is licensed under the terms of Third Party Software Licenses. You understand and agree that, although provided to you by the Company with the Software, your use of Third Party Software shall be and is governed by such Third Party Software Licenses. You understand and acknowledge that Third Party Software is being provided “AS IS” WITHOUT ANY WARRANTY, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. If there is a conflict between this Agreement and any Third Party Software Licensewith respect to Third Party Software, the provisions of such Third Party Software Licenseshall prevail.
  22. \n
  23. Export Restrictions.You agree to comply with all export and re-export restrictions and regulations (“Export Restrictions”) imposed by the United States or any other country in which you conduct business. Without limiting the generality of the foregoing, and regardless of any disclosure made by you to the Company regarding an ultimate destination of the Software, you represent and warrant that you understand the U.S. law currently prohibits the export or re-export, directly or indirectly (including via remote access) of U.S. origin products and technology to certain proscribed countries (including Cuba, Iran, Sudan, North Korea and Syria and/or any other country that may become subject to an embargo by the United States), entities, organizations and individuals, without prior authorization from the U.S. Government. You will not commit any act or omission that will result in a breach of any such Export Restrictions. Your breach of this clause shall constitute cause for immediate termination of this Agreement.
  24. \n
  25. High Risk Activities.The Software is not designed, manufactured or intended for use or resale as on-line control equipment in hazardous environments requiring fail-safe performance, such as in the operation of nuclear facilities, aircraft navigation or communication systems, air traffic control, direct life support machines, or weapons systems, in which the failure of the Software could lead directly to death, personal injury, or severe physical or environmental damage (“High Risk Activities”). The Company specifically disclaims any express or implied warranty of fitness for High Risk Activities.
  26. \n
  27. Consent to Collect Data.You consent Company to collect and use technical data and information from the installed application anonymously. The collected information include but not limited to the version of the Software, Software configuration, devices added or connected to the Software, total session time, Internet/network usage, and hardware specification. The information is gathered to provision the Software usage, updates, devices and configuration so developers can enhance and improve the Software.
  28. \n
\n

The data collection can be disabled from the Software if you do not want the data to be sent.

\n
    \n
  1. Termination.The licenses granted herein shall automatically terminate without notice if you fail to comply with any material provision of this Agreement. In such event, you must immediately uninstall and return the Software, including all Documentation and original media, to the Company. This shall not limit or affect any remedy available to the Company for your breach of this Agreement.
  2. \n
  3. Miscellaneous.
  4. \n
\n

(a) If you are a unit or agency of the United States Government, the following applies: The Software is provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the Government is subject to restrictions as set forth in Subparagraphs (a) through (d) of the Commercial Computer-Restricted Rights clause at FAR 52.227-19 when applicable, or in Subparagraph 252.227-7013 (c)(1)(ii) of the Rights in Technical Data and Computer Software at DFARS, and in similar clauses in the NASA FAR Supplement. Contractor/manufacturer is Digital Watchdog.

\n

(b) This Agreement shall be governed by the laws of the State of California, exclusive of its choice of law rules. Any dispute between you and the Company regarding this Agreement will be subject to the exclusive jurisdiction of the state and federal courts sitting in the County of Los Angeles in the State of California. In any such action, the prevailing party will be entitled to costs and attorneys’ fees.

\n

(c) You may not assign, sublicense, or transfer this Agreement, the Software, or any rights or obligations hereunder without prior written consent of the Company. Any such attempted assignment, sublicense, or transfer will be null and void. The Company may terminate this Agreement in the event of any such attempted assignment, sublicense, or transfer.

\n

(d) This Agreement is in the English language only, and will only be provided in the English language, which language will be controlling in all respects, and all versions hereof in any other language will not be binding on the parties hereto. All communications and notices to be made or given pursuant to this Agreement must be in the English language.

\n

(e) The Agreement constitutes the final and complete understanding between you and the Company with respect to the subject matter of this Agreement. Any modifications or waivers of this Agreement must be in writing and signed by both parties hereto.

\n

(f) If any provision in this Agreement is invalid or unenforceable or contrary to applicable law, such provision shall be construed, limited, or altered, as necessary, to eliminate the invalidity or unenforceability or the conflict with applicable law, and all other provisions of this Agreement shall remain in effect.

\n

(g) Failure or delay on the part of the Company to exercise any right, power, privilege, or remedy will not constitute a waiver of, or bar the later exercise of, that or any other right, power, privilege, or remedy of the Company.

\n

(h) No rule of strict construction shall apply against or in favor of either party in the construction and interpretation of this Agreement.

\n\n\n", + "eulaVersion": 1, + "packages": [ + { + "component": "server", + "platform": "linux_x64", + "file": "dwspectrum-server_update-5.0.0.35271-linux_x64.zip", + "size": 88614643, + "md5": "e027ae6b5f4420ac77dfd76f4e502f99", + "signature": "DF54oM/oYcBZ6h6gamUt/QfGoV+yxA/yfkrDZyqqhhXdg3uALI5kRYQy7A32EvmThB1DQ2pi/5Y7s4EHAg4kJWOgPZiwaO1cQ8++gai4YVes2duzLFqkUHCFAvoD61QQSps+Wbtq/agcH9nR0RrnhvYdvG0kRkcuOrESWN3XkuC0g+LxczUpuDXYtZ3SMjuoQ60UTkrQDcMsb/kcfFEzyIL+aRlV2u1tBbdYjADP1opQjPAtbVjrfzlTXTz75Hn1HldtXdO8vByIm9ljhKFETs0FFjXj6YUn0TdzLffw0Bs/IFyrZ7KcvQW7M62rKN8z+1u0KvwQKatrh8+ZFCEZAFx8MH+gUy5sTcyRrvInrmvDnL1r7WVmXv7p3gWFLSewtMDlcBOK7X+rW4+c5pk5ltQWM5G2V50lYsVOOPjjFvoDSpkdAOksXRWg06unS3tU2jDRlFEEE46HLLueuFH7a2fTG/IX8LcRE/Pcfn0kDHEzjB3lZnwDahGwH42CynzaZUXRW6KH73MwcwYDPO/fjBzB7YrXhBicMCHxASPjDMD3ml/7lbOdqGPeeknbNhnfVkgze+S0nr9OIaTQ2CcVfIHRmmjtbMG2cIo7X+3wgdUaSfSBOGoZA/8yIyAJehiz9XlpjOEpFD2P+ndGQsIsIIExXhNfOxUmdjsFLye31IA=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "16.04" + } + ] + }, + { + "component": "server", + "platform": "windows_x64", + "file": "dwspectrum-server_update-5.0.0.35271-windows_x64.zip", + "size": 97987920, + "md5": "c809fdd6cd162e368a8593b211c35e5f", + "signature": "AmCSSckelnzgMzganq9H/m8Ekwg+fmbVGByxGDXIvQljFWw2D/B17nnEVdfH8vE5NVPO4Co7qW3xdJMSGJrJCaHEf5ijs5SJe3LEGDAiNjZEsGH9MFMJNnOPyS3W9PysLDL/5SFSHYcXkgeug+erlnuC8MAkVtb/GDq9s/8nqIY/KWKQ3hNn67uq5lJe+8i4A9GK5jailev355JnGvGrs3+9Hu47+dSWMl2dYq2IYsNsO2bDbknaUpIctkTxhQ/RDBuyazOHT/67P5IwedTemzWvo5/oJqPaU7khx82qKqEgifmZXLD8eZX3aPwTq3NtXbdBmrqcHEmdpG0s+pVhWHYUUjlPpyQ0p+VqeJKqC1PvecGdZpMVkj7Ix8/FlJbu5TtO+0SY6bg/h1hgQHD2Vtf8PqapeyrKigprPONz6JVYIvmkOTLIrfaEOrFhsIF1cNzYXxuQI1T/U/q6Okisr4P88Wkcd/ZDAbj3Z95fyRxFEozsWOa2s6WA27+0NRKoNR08GzhR0LIc1HqKOL9pwgNAwcDnqPca0R35tqDDGOUj+Czt5mE6dgnTSG4XNPr0G1tsU9Xt5+4dhYRIfkqFYyML42kq9SVfZLCL3GbLbL1Lpo09RKbUwu0zJllCk8itRb6FASevcZPpDNOYhWo08ijABEiYIdhUcOdKhyt9IWw=", + "variants": [] + }, + { + "component": "server", + "platform": "edge1", + "file": "dwspectrum-server_update-5.0.0.35271-edge1.zip", + "size": 85437501, + "md5": "018f57ac6160f849c7eaf51ceea7d258", + "signature": "mkWJRgn+t1e+5811FaPMSSokpwRrtc4L0UcIBDvdpVBTEzhtBcl7FzJBJHP9ljhHW71eEnev4j269C/S4FNn59m01+WsGdiiqd29ReH7zw8mL+YUjjitlzpCNSnSh3fwcvAQ5piLyNcMgdEXFkoZE/A8T8BzKgw6dSfkLhM/5LFftdB8BS1QG4poA83fl1XJqCCDzXt0mgDcCQGwj6s9fEnCe56eB8dDFwoA8M4LpAdojdbKuvxXSi/VRa7CYwvyvwQQaeqSJ44PLD/NrNjL68vl8Mm2HW95goIovE1MpycUCoB7Hb9anfxnP5diWsA/537FhWUSABEjml4L2j6ncGZC3ZatXBpC4Jr2ewPuUBaBJQMFY64fYMPy5jcL2GdiX4Kjl8S0XDUDHmywxl7eFTjuB9LPOjLUWWHd9D3w8vi40OefwIbHfGpE2gNKCb6pkvmNgRvwn6a47hNJ1iTNGuOigq0b/pug/8WxZDSdHkMuq3M61l0MPeucydD3AGWbpMv+teceYTd3DH/gYksM8++jRccErGPH5LOpzlHwZnHorHfyp1/iVoKVL77I5aOzU2vgpiv+sJNHvEj2PIPg7pyUjAZbHvFMwusfm2w1fCGla9Df/lmiDAEogVG4qqBJuKn8lYe1/xZwYH/L4oQ3+zSHc/+JeXwmo2ynaekZzYk=", + "variants": [] + }, + { + "component": "server", + "platform": "linux_arm64", + "file": "dwspectrum-server_update-5.0.0.35271-linux_arm64.zip", + "size": 143427373, + "md5": "589df20cab8d54a1eb1c909cfd703f75", + "signature": "RuCn/6jOR5z5wf6JEspzPB1HyDMTYauUKxqBkIrxbS8YEO8MVpd2jrRd5hkd0u58fZ/9CZIXHFplc9887K3MPkjgn1YoDSO6lv5sieLxO8lM7LYtaFQs6H8/YIv7QZnooyOv1cgLLkRvb/LcLmEkbIBPHKLQ8ChDUJD6bmFyolxcGRjFfiAyFRzEsqBIwiK/gr6JmyC5DBqFbz5MyBafMt/BwWXrH4e0e9zgDSulBJC2FLoUKnewEtCN6TUDD248d2hGrUz4Q8JXIzcGjSxt0jwnc3c7TNpkVxfuy0e5Ob7KCTWnjX/LNzvD6bimMBWzxPQXIlvn1OmLsv4fMm5Qzz4ZX0FWTOxZsZO8B5VWfVBYJaGtwPhXOZhNt3IsS9ffTKG8K9C0taTQTi8JQVbZHgwjzRY87fuC9NxGYXYJ3HXriaQ/VB8s6mvy7GOee+cqux145B09pkndJIy46QF0VTypmDZC6G8fCCY8tT9tOtJcksgizh0QchwNNik+KInx1wgxds4vTN8ld9CmtWoJEfB0ydmBGHL0/vZksj+o9+ds8nD7gfTfdo+4RuMiDmzQX5sOehg8u1fAHuGVUOydqOGzgHa9WPKdtQ3CpzipKLnu4BRJRqpE5LJ0KXAM1yTxmskINIzODrlZPRkeCG63uarbJSlQlhEHajLGONyRJNk=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "16.04" + } + ] + }, + { + "component": "server", + "platform": "linux_arm32", + "file": "dwspectrum-server_update-5.0.0.35271-linux_arm32.zip", + "size": 169064419, + "md5": "39e11cb1c489b0993380dd8b2a2e99a4", + "signature": "Kd2gdCR7oFSNLfrqb8XZPf0NmuNZ6LpHznAtNoDL6DqpnWBXsZ2YdqtAvYmpWX0EoqJZ7TRoyjyEKiu06kalgOXE+xFyEhAYTaQp50MIpkq3kutZ8l5rnkum99WvCW0NnHPBIF1OlO7xBFEQMWWDSSmooVaR9EqxhajIsYBj+8mzBTvOnNCn5cDgv1/4V+GCSIjHAeDiLE3CB0hr8yprzKKhKfdZQMTYfHziVspjDLTgeSx07cehEhYqn9uJb35QZN4ily4e3htbXPPLbPNMP2/Sn97mJYFRm7tMs8xzUImUE5gGV5Fc6SFmiYK/otpYtxUyuexRTZU5w7dHQXuRksIE3b8BQQ8CRFhRd0zN534hm9whNRI0IYnBPOG2oaEA1mWsTEvAj+9Gzp8WsIKCHXRE1Ybicqa8D7tdO781sBgrW5ltiEZEmZWOo/p5Z/boC87UTib1s3aQG10lccW8srL4skirydboipFPE3QYAjcnOc3eocRN4A9LHtDbgCTmME2cRAAAvIYDCwiVSfnbuWWrkOSGDXv+A06OwCB1l+OS2qUecaHg+QoiFYGXffBStNCU0RyVg8QWbeOjxBD2xcpFquypyo6+jxXO/ZPZ5AUXepBB1iS5iIWpqpGc0RHtlDQSRdse1xFVshc6CCOvk1H0EO9RqDq1oxmv+PATo7g=", + "variants": [ + { + "name": "debian", + "minimumVersion": "9" + }, + { + "name": "ubuntu", + "minimumVersion": "16.04" + }, + { + "name": "raspbian", + "minimumVersion": "9" + } + ] + }, + { + "component": "client", + "platform": "linux_x64", + "file": "dwspectrum-client_update-5.0.0.35271-linux_x64.zip", + "size": 268837878, + "md5": "265a473af3a8bda35984e924d3783e05", + "signature": "MLGEe7AcSZbch2+Bvh2RUhdydCbuFXOnRjpm9rJWsp/eOYVq2ZXsyOwoEV6sZexCAxfQS3LZg8xPnGTklJE6V1FP2Qc7GnxW/jPAhRKoWeP931+m//ZRfvkg4adLTLId9fVgvtX95btqrqdqLUUBSoWHAZoO/XrliHh0JsnSACbF1umv1l6byhxJAEHVKWEFzc7aTX/ibsfnZljDI1qLONtRWIOE8Gg+gct65B0uq/FLr8FcLfocD1v4nkRQkAtBB4mk1TjOsIyKMweOhnuvwNE5XUnx6IjBUDuxMFYAkyRfsZ1IbydMUzwcZHUnCAwbo6XbloJsu1yH55jYwnJNlHGJnPC+xCyThJiWvXfypgp/1WqS35yMFH2e3TSZxKK7SUiF7f8Hxkjc7iYM/Ba71KvsoU07Q+nE7if1EEUOiHqeUCrwYZg0R/cSUiMfqfx0A3OuuVeQTQKTPZ35VI/Wgry7I0buV9kpugHNshQ2U2ZTZYG8ipMC0HdoXaS//5Tfh+iK0xqhdTjzWjZeNwDPs9F+ap4ICzkWK27qgtszNM0XgFczkGE6g+I/V5oJH3f32qn+fi2YGJOLGer2samoy8lnLzNamLszUSuUojLvlEIAdR5cjsErwln/srjIFQb1Wuy8+sMqaV2DYHSexuM7oQKK5LMaNdlACZ89O+XXZzs=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "16.04" + } + ] + }, + { + "component": "client", + "platform": "windows_x64", + "file": "dwspectrum-client_update-5.0.0.35271-windows_x64.zip", + "size": 153542461, + "md5": "2ce20c6bb040a39c2c76d63b5e072c08", + "signature": "GNdBfjjZKscX1+wya6AgEkCTfM9fg0H/T+MW6QtAINhGI+iaAyJfAKF9/6KZ2KiotkUud9mnmr3gi2Bkq/GdxpTbv6EOidkzgcvbSJTEATf6CDI7F1aJH19wQFzor05Y31y9TyMJWYHIO8Yue9DH5xoVUAZptbOxHyil3gXuQVk8LAZZWd6pNoA8ABQS5V8WBf2zzt4cSvxMlc1aKf2PPiimyk4Sklq/ZEt0N5TqWkxTnReA/32IoZGAbYFeaWAhpuZhP+BixYuDEdLoR/3WZ/SRPZei4hg7/g4gUPB3+isIpiE+3LWY3O/BkBQm1ZQuayCbeJwHiUGlUfQ+gQwVbPJAEUbJ6TaOYgUeIao+pBXPFa0LQtG/ISJcbsG0xKbGcOox1NRT81WGMynjrfxLGiNPGkU+YurDF3CN8Cctmd1wkqbY32g4XWw4BsaHwTCRKRdJdaoowbYBSsjiCYQ/X/FJChcVFcKcQNhLVc/aVSdy9PTp4kfWWpAdPzaXI7TgSSEWBGatKkE0av6xshWCMZT0lbk1aQr8iyq/LjamUJwPVDsxtTas4ppfJDV5VgqVfvEazf4TyMlbHyi4Db776caZWFSQto6szh2JTMIfq35oaJTL+nCXnD8AkTS3jot/qLAUFBY4hCV0FM2W5U7G4+kM4dotVxbTdaK4o1UhnwI=", + "variants": [] + }, + { + "component": "client", + "platform": "macos_x64", + "file": "dwspectrum-client_update-5.0.0.35271-macos_x64.zip", + "size": 158000043, + "md5": "4bb2e9498267f51fc7057c2695bc417d", + "signature": "TCxNIKAlgbVKXXUessdjx0manmIFgYuKPjbnLMhe05MQ1Gg7nhUtfthQipJ8R+ewiy487L1KxSfu5uUCBAu7zcMnckjtM2t/owYv7PLb00HnqLzXfY6ZhfGQ9gVHJbHhANZDtsRlzAdFX+263+l3O3Uh/5Ci7rkIsjW7KmiIlC5yePipK3Pnp7R9VR8G4Bp2ZVlPoseRD4ovg+HGN4xABCZk7A784KE66/nEnXGUuUFofij6O30+YZYvgk4GTzQ5CX4iwko2y/eXFSUm7ftCN+FV55YQuShwgocV/uTmVD2git+mR4cBELBIfVOkTkwKiwMij+h3DhJPZnzvUC3qcQbiejRN+6sVA48z6+RcI0nmG+HVvkOesceqNQ+UeG7kRFsgBQXM9f2twy2f5ui8u7eh4/YXenCvzk7tOzjHeAcTjuorWJsL5f7dd4bl7EZVwo7itS0jmKitSmQvaUBxEAfMEjRTpgmZnk0PH3wHT75WyE9Bu9mVG0rFA5E1HUcOmQg5qyzv9C9mPohUSMfZSRbBNcrBr2kcikzdZS3bj/TsGhP6c10SFts5M0npdtNZiBMIrTInRKaPVfihG3wYaCNAjJO0yR7PpXet5qkAP0bUWh0E4yEWdm3vKyFrLMfkui7Xr8r108n4TlZflYOmY6hK7+o9rIJU+LqvFZyo8hc=", + "variants": [] + }, + { + "component": "client", + "platform": "macos", + "file": "dwspectrum-client_update-5.0.0.35271-macos.zip", + "size": 158000038, + "md5": "c3b126cb6d4558f5c38b52e423269dd0", + "signature": "bHJUbQtwFm/PkkFpAAbIjnw/iWPDZX8GMkmUmJSXB8VGkxy5kITi9iV9P1iHc2GhBM7hXV2ihYuEhDoRfI2p79oe/a4Aa1lT/iIS7x1EA6hQ3VeD5DwWVfP3OvDFv98h0F/6QAvxbCDAdF12YTMLWn/DBQ1kgL4dZz0wgkxrrvDwSuaRp2Z2RGJGnX85A4CyNrgujWYRMtAhZ9TusVqF5LSVE2BZoBqGa/p26v3SbKy7b5KpHu94xyKtFvL0JTSzL0LBZj3BfupSXJMuGKdaLnlbZlN5L+aoEO1+uKuht7qMVKI3fiXVlAoH0ZjadBAcyb+NEsYnz970TGq2tUm0RXpQkGdF5f/QW8xE2sEDZEDCER66jRceIqig6e2OChECYRJ4m8s93hwcNm0lc9IfqkfsN/V2c/S4D9pOCx0L2fwGpEVawqTAFAwJnUqiOgDwWut3iTXair22ani67d6wreulqQyEBoh9wkNph21toaFq3FX9+ChRGXW8oe63m4WdgZD/n9zdlgyhbg03HbO8CUJI5OyiMdrQxhkyyPlz+7r3y2RXL+GrnKlX3mvXtDvC4cFQ4Fz+9mKZ72A6yX/NN72C+ca73Or4XWOPrFwrR0z98oWLozoAaFiexYz2+gIibNW4uPoVUk9f62Zdb7mMJsnmTcMouRjAun9xOF1SAig=", + "variants": [] + }, + { + "component": "client", + "platform": "macos_arm64", + "file": "dwspectrum-client_update-5.0.0.35271-macos_arm64.zip", + "size": 144560236, + "md5": "c2cc87a5915d02d9b74354838236a083", + "signature": "zBV4SXYDfpq9+veBn3wV4NyzSQtt6b7h0kIKgNOyUYXxilQEnjqxkAoqEe6YZPBdifQrJT7OSzLUD11zxWg2tHpsQB/jsX2QaItZO7AWYaMxRotbZ25yzcmCW/Tx+cHDyG0xNP5R6tE/3MQR+8FepnoLP+DgAFFyrCl+U3kuW74H6hYSS3RlsTVoz4/41wxc/jkleYKEQHO/LRd2j3+T+DQTGAPaBBTeRPR4GcOpdTzSMtNm6QA6bZQmj3Wh5wuKQc8fw5k5Ixjcc+TAj7gYMLBvf4jWgD+4soYqv/XftVbr9FL+WwBTYjKTjJA89n/MHXuYTV4rsU8xYGMiBloWZQwO/4kVLEJcbSKy7NpEP61qcgGj19TUK/aEhD3a12sgcaUzvP3sOc8c65yy66CkqKZWIUcPiGYjvweltvWBmpSBG2dH92GDBO9tU9Ivoi9PPrnoxxN2YcVghDZVmirXi+WlIJy9NDH2xONN87im4bsMZB1V4dMrbhQSTXIq0rKzVKWU37JrgXml1qsjl2zqUt4xl30DxPwYoH9AIxk++uhMlGmH16kgVywlOBUjyV6/NEc1MM0ac3zwqTYgy6edpGufLRjttg6oKLMZ6Xc55XMvILkzU7V3AQqW1L0qJJSHuwUWhERhQzLE7sIKgMQBNFtte0MxjpseEJ0xxg/4wSY=", + "variants": [] + }, + { + "component": "client", + "platform": "linux_arm64", + "file": "dwspectrum-client_update-5.0.0.35271-linux_arm64.zip", + "size": 273433878, + "md5": "e6b7170ff2557c6b34f76761d16a6794", + "signature": "m1KZMp1DTuD7vwKhnUzMlL827EhQHYs1Y5Hmg/ufvfw0fgCCES+b2j4XWUGv2Y1L+MZWVc5lhrydcTCooVL57pESub8SSaKd8BIqjrjv4ZitOWzSPlN/kaOylV84NM8Zf6PTPl+/hxkjwFGqlI3RPp6nw1AmUIJOJLNM1YxfnRE8COBCKqfey5kWLclGnqGvfRcyQ8BSAiL4o1rnewm0773faeX7JKoQTJ5xwPxqVyinIjZxzXZK5DEuC9QHj+b/uwiATAzhcObij3xmq5BR8xTg3EboaKHeAMu8zDQhTHLhdnVwyeRZtLo961GbuavaMYJ+nxEd6Nb8/WiM8dUUAL0eVo6ngHJFxJ9ChtFe6Bz55cZNsCxZdkhSzmrVk3uH9puWJ98HBQYlCe2GEx/9W+Lm10qOhpQ1B3BdCYqK6JcMvtnfZMYtvYlaJ2UuDm+tFWCJhidjVbGDRJ9DQ/q+BoiknslPndfYZvARtOP8/CxB9gUhD820yAzjKhjmx2NeiO/TEg37w8oWjdaaXegmWq2fmq3Hue13RQSh52pgwLqHEeikunxo36TgKe0aY+3lpANzSgzIzyO7ngbiJgTgsd9kCnXRuyLfGVmXGZcpFeK5ptIAU5bU5nmcQm628JH/Cjeln+WhQFFThM0w3fP0DrQ84KbuCYvp5keuue8j+D8=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "16.04" + } + ] + }, + { + "component": "server", + "platform": "nx1", + "file": "", + "size": 0, + "md5": "", + "variants": [ + { + "name": "", + "minimumVersion": "3000" + } + ] + } + ] } \ No newline at end of file diff --git a/Samples/Packages/NxMeta.json b/Samples/Packages/NxMeta.json index 0c6717c8..f50ff01a 100644 --- a/Samples/Packages/NxMeta.json +++ b/Samples/Packages/NxMeta.json @@ -1,166 +1,166 @@ -{ - "version": "6.0.0.38086", - "cloudHost": "meta.nxvms.com", - "releaseNotesUrl": "https://meta.nxvms.com/downloads/releases", - "description": "", - "eula": "\n\n\n \n Nx Witness EULA\n\n\n\t

NETWORK OPTIX, INC.

\n

END USER LICENSE AGREEMENT

\n

IMPORTANT – PLEASE READ CAREFULLY: This End User License Agreement (the “Agreement”) is a legally enforceable contract between You (either an individual or an entity) and Network Optix, Inc. (the “Company”) regarding the use of the Company’s software programs accompanying or provided under this Agreement together with the SDK made available by the Company, associated media, corresponding Documentation, technical configurations, and technical data (collectively, the “Software”). “Documentation” means the user’s guide and other documentation (including print and online), if any, provided to you with the Software.

\n

BY CLICKING “I ACCEPT” BUTTON BELOW OR BY DOWNLOADING, INSTALLING OR OTHERWISE USING THE SOFTWARE, YOU CONSENT TO BE LEGALLY BOUND BY ALL THE TERMS AND CONDITIONS OF THE AGREEMENT. IF YOU ARE ENTERING INTO THE AGREEMENT ON BEHALF OF A COMPANY OR OTHER LEGAL ENTITY, YOU REPRESENT THAT YOU HAVE THE AUTHORITY TO BIND SUCH ENTITY AND ITS AFFILIATES TO THE AGREEMENT, IN WHICH CASE THE TERMS \"YOU\" OR \"YOUR\" SHALL REFER TO SUCH ENTITY AND ITS AFFILIATES. IF YOU DO NOT HAVE SUCH AUTHORITY, OR IF YOU DO NOT AGREE WITH ANY OF THE TERMS OF THE AGREEMENT, YOU MUST NOT ACCEPT THE AGREEMENT AND MAY NOT USE THE SOFTWARE.

\n

In addition, certain third party programs or code (“Third Party Software”) are being provided to you under the terms and conditions of a separate end user license accompanying such Third Party Software (“Third Party Software Licenses”), which will be provided to you by the Company as part of the Documentation or may be made available for viewing by the Company at the website or webpage at https://meta.nxvms.com/content/libraries (the “Third Party Site”). If required by any Third Party Software License, the Company will make such Third Party Software, and the Company’s modifications to that Third Party Software, available to you on the Third Party Site. The Company may be required or wish to provide you with certain notices or information about Third Party Software or Third Party Software Licenses and you acknowledge that such notices and information may be provided to you by the posting of such notices and information on the Third Party Site.. BY INSTALLING, COPYING OR OTHERWISE USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS AGREEMENT AS WELL AS THE TERMS OF THIRD PARTY SOFTWARE LICENSES. IF YOU DO NOT AGREE TO ALL SUCH TERMS, DO NOT INSTALL OR USE THE SOFTWARE.

\n

1. License Grant. Subject to the terms and conditions of this Agreement, the Company grants you a non-exclusive, non-transferable, non-sublicensable, limited license to install and use the Software, in object code version only, for your own use subject to the restrictions contained in this Agreement.

\n

2. Backup Copies. You may make one backup copy of the Software, provided your backup copy is not installed or used on any computer and further provided that all such copies shall bear the original and unmodified copyright, patent and other intellectual property markings that appear on or in the Software. You may not transfer the rights to a backup copy unless you transfer all rights in the Software as provided in this Agreement.

\n

3. Ownership. You acknowledge that the Software and any copies that you are authorized by the Company to make are the intellectual property of, and are owned by, the Company and its licensors. The structure, sequence and organization of the Software are the valuable trade secrets and confidential information of the Company and its licensors. The Software is protected by copyright, including without limitation by United States copyright law, international treaty provisions and applicable laws in the country in which it is used. You acknowledge that the Company, or its licensors, retain the ownership of all patents, copyrights, trade secrets, trademarks and other intellectual property rights pertaining to the Software. You will take no actions which adversely affect the Company’s intellectual property rights in the Software. This Agreement is a license and not an agreement for sale. No title to, or ownership of, the Software, or any intellectual property rights subsisting therein, is transferred to you.

\n

4. Restrictions. The Software is licensed solely for your own use. The Company and its licensors reserve all rights not expressly granted to you in this Agreement. Without limiting the generality of the foregoing, you shall not nor shall you permit any other party to: (i) make copies of the Software except as expressly set forth in this Agreement, or (ii) disassemble, decompile or translate any part of the Software, or otherwise attempt to reconstruct or discover the source code of the Software, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding this limitation, or (iii) modify or create derivative works based upon the Software, or (iv) externally distribute, sublicense, resell, encumber or otherwise transfer the Software, or (v) rent, lease, lend, or use the Software, or any part thereof, for timesharing or bureau use, or (vi) allow a third party to copy, access, or use the Software (except as expressly provided in this Agreement), or (vi) alter or remove any copyright, trademark or other proprietary notice which may appear on the Software, (vii) take any action that would cause the Software to be placed in the public domain or become subject to open source license agreement, or (viii) use the Software in any manner that violates any statute, law, rule, regulation, directive, guideline, bylaw whether presently in force or may be implemented by federal, state or local authorities.

\n

5. Paid Features. The Software is made available to you at no charge or fees. However, the Company may offer certain premium features to you upon payment of a license fees determined by the Company in its sole discretion (“Paid Features”). In order to use Paid Features, you must agree to pay the license fees plus any applicable taxes through one of the payment methods accepted by the Company. Upon receipt of the license fees, the Company will either provide you a license key to activate the Paid Features or allow you to download the Software with the Paid Features. Activation of Paid Features may also be regulated through the use of transferrable license keys, and such license keys may only be transferred within your organization subject to the limitations listed in your purchase order or other documentation describing the Software and/or license rights obtained in the Software. You expressly acknowledge and agree that this Agreement will govern your use of the Paid Features.

\n

6. Updates. Except as otherwise expressly provided herein, this Agreement will govern any updates and upgrades to the Software that may be provided to you in accordance with the Company’s then-current maintenance and support policies, unless such updates and upgrades are provided under a separate license agreement. You may use such updates or upgrades only in conjunction with your then-existing the Software licensed under this Agreement. The Software and all updates and upgrades are licensed as a single product and such updates and upgrades may not be separated from the Software to exceed the scope of your original license. Any other software that may be provided with the Software that is associated with a separate end-user license agreement is licensed to you under the terms of that separate license agreement.

\n

7. No Support. Except as expressly required by applicable law, nothing in this Agreement entitles you to any support, maintenance or new versions of the Software. You may contact the Company to determine the availability of support, maintenance, and new versions, and the fees terms and conditions that would apply.

\n

8. Disclaimer of Warranties. THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OR REPRESENTATION OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. NEITHER THE COMPANY NOR ITS LICENSORS REPRESENT OR WARRANT THAT THE SOFTWARE WILL SATISFY YOUR REQUIREMENTS OR THAT IT IS WITHOUT DEFECT OR ERROR OR THAT THE OPERATION THEREOF WILL BE UNINTERRUPTED OR ERROR FREE. The Company does not warrant that functions contained in the Software will meet your requirements or that the operation of the Software will be error-free or uninterrupted. The Company is not responsible for problems caused by changes in, or modifications to, the operating characteristics of any computer hardware or operating system for which the Software or any upgrade or update is procured, nor is the Company responsible for problems which occur as a result of the use of the Software in conjunction with software of third parties or with hardware, which does not meet the Company’s specific requirements. Some jurisdictions do not allow certain disclaimers or limitations of warranties, so some of these may not apply to you.

\n

9. Limitation Of Liability. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW EVEN IF A REMEDY FAILS ITS ESSENTIAL PURPOSE, IN NO EVENT SHALL THE COMPANY OR ITS LICENSORS BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE OR THE PROVISION OF OR FAILURE TO PROVIDE SUPPORT SERVICES, EVEN IF THE COMPANY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN ANY CASE, THE COMPANY’S ENTIRE LIABILITY TO YOU OR ANY OTHER PARTY FOR LOSS OR DAMAGE RESULTING FROM ANY CLAIMS, DEMANDS OR ACTIONS ARISING OUT OF THIS AGREEMENT SHALL NOT EXCEED THE FEE ACTUALLY PAID BY YOU TO THE COMPANY FOR THE SOFTWARE, IF ANY, NOTWITHSTANDING ANY FAILURE OF ESSENTIAL PURPOSE OF ANY LIMITED REMEDY. Because some states and jurisdictions do not allow the exclusion or limitation of liability, the above limitation may not apply to you. In such states and jurisdictions, the Company’s liability shall be limited to the greatest extent permitted by law.

\n

10. Sole Remedy and Allocation of Risk. YOUR SOLE AND EXCLUSIVE REMEDY AND THE COMPANY’S SOLE AND EXCLUSIVE LIABILITY ARE SET FORTH IN THIS AGREEMENT. This Agreement defines a mutually agreed-upon allocation of risk.

\n

11. Third Party Software Licenses. Third Party Software included with the Software is licensed under the terms of Third Party Software Licenses. You understand and agree that, although provided to you by the Company with the Software, your use of Third Party Software shall be and is governed by such Third Party Software Licenses. You understand and acknowledge that Third Party Software is being provided “AS IS” WITHOUT ANY WARRANTY, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. If there is a conflict between this Agreement and any Third Party Software License with respect to Third Party Software, the provisions of such Third Party Software License shall prevail.

\n

12. Export Restrictions. You agree to comply with all export and re-export restrictions and regulations (“Export Restrictions”) imposed by the United States or any other country in which you conduct business. Without limiting the generality of the foregoing, and regardless of any disclosure made by you to the Company regarding an ultimate destination of the Software, you represent and warrant that you understand the U.S. law currently prohibits the export or re-export, directly or indirectly (including via remote access) of U.S. origin products and technology to certain proscribed countries (including Cuba, Iran, Sudan, North Korea and Syria and/or any other country that may become subject to an embargo by the United States), entities, organizations and individuals, without prior authorization from the U.S. Government. You will not commit any act or omission that will result in a breach of any such Export Restrictions. Your breach of this clause shall constitute cause for immediate termination of this Agreement.

\n

13. High Risk Activities. The Software is not designed, manufactured or intended for use or resale as on-line control equipment in hazardous environments requiring fail-safe performance, such as in the operation of nuclear facilities, aircraft navigation or communication systems, air traffic control, direct life support machines, or weapons systems, in which the failure of the Software could lead directly to death, personal injury, or severe physical or environmental damage (“High Risk Activities”). The Company specifically disclaims any express or implied warranty of fitness for High Risk Activities.

\n

14. Termination. The licenses granted herein shall automatically terminate without notice if you fail to comply with any material provision of this Agreement. In such event, you must immediately uninstall and return the Software, including all Documentation and original media, to the Company. This shall not limit or affect any remedy available to the Company for your breach of this Agreement.

\n

15. Miscellaneous.

\n

(a) If you are a unit or agency of the United States Government, the following applies: The Software is provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the Government is subject to restrictions as set forth in Subparagraphs (a) through (d) of the Commercial Computer-Restricted Rights clause at FAR 52.227-19 when applicable, or in Subparagraph 252.227-7013 (c)(1)(ii) of the Rights in Technical Data and Computer Software at DFARS, and in similar clauses in the NASA FAR Supplement. Contractor/manufacturer is Network Optix, Inc.

\n

(b) This Agreement shall be governed by the laws of the State of California, exclusive of its choice of law rules. Any dispute between you and the Company regarding this Agreement will be subject to the exclusive jurisdiction of the state and federal courts sitting in the County of Los Angeles in the State of California. In any such action, the prevailing party will be entitled to costs and attorneys’ fees.

\n

(c) You may not assign, sublicense, or transfer this Agreement, the Software, or any rights or obligations hereunder without prior written consent of the Company. Any such attempted assignment, sublicense, or transfer will be null and void. The Company may terminate this Agreement in the event of any such attempted assignment, sublicense, or transfer.

\n

(d) This Agreement is in the English language only, and will only be provided in the English language, which language will be controlling in all respects, and all versions hereof in any other language will not be binding on the parties hereto. All communications and notices to be made or given pursuant to this Agreement must be in the English language.

\n

(e) The Agreement constitutes the final and complete understanding between you and the Company with respect to the subject matter of this Agreement. Any modifications or waivers of this Agreement must be in writing and signed by both parties hereto.

\n

(f) If any provision in this Agreement is invalid or unenforceable or contrary to applicable law, such provision shall be construed, limited, or altered, as necessary, to eliminate the invalidity or unenforceability or the conflict with applicable law, and all other provisions of this Agreement shall remain in effect.

\n

(g) Failure or delay on the part of the Company to exercise any right, power, privilege, or remedy will not constitute a waiver of, or bar the later exercise of, that or any other right, power, privilege, or remedy of the Company.

\n

(h) No rule of strict construction shall apply against or in favor of either party in the construction and interpretation of this Agreement.

\n

(i) This document was originally written in the English language, and the English text of this document shall prevail over any translation hereof.

\n\n", - "eulaVersion": 1, - "packages": [ - { - "component": "client", - "platform": "linux_arm64", - "file": "metavms-client_update-6.0.0.38086-linux_arm64-beta.zip", - "size": 282726618, - "md5": "79fb0e0d413f76fd888b9f1b69018b46", - "signature": "g7V7D7N7K09qTq0f1h2KRPK3El+HGUbYD7sTw0cw5ZcSPfWqchjFoqmQwseJTvyvjsmefW8dL/XeQTp6zrKJf/6+fA9j7BQoXz6IXWme8m5reob9ZnEhOuCrHNi2zf3JO9+PCwrVAkV2AVlIMMTr49gmAPvVn9c0O/jilqz6RtaZm5CWYjrdXUyPIbqer/aNxKpNNLfHJ4GkBEospoRpLqofxBXM1C2V9/R9tzqMoDO9R81fJjnztrqyoBRl1BdOPxTojNQ7D7AgAxOnsE8nSaq0HIn2ZvBmTY9q7OCv28jk2/6zUwKYoebw9SAbKcMa8O/nqZtQwU7EtGI0GKOhMJ1wUzKb2ymuhPrVrfeMhoB4FPjTfy2aUIfVDr7FgHePaxSpywowYcApdd406bW4kUoyp24dcz5NgTKoZJiMbubRWVpeB5DKcSFKgWjPE8Emaw5LCwJ+urlEpO8kcwaUDeBJAeB1VHYbknfoHVyWZtld98hzkhqkHQDCVl+gfHlx2BPEkAf4HdOIYN6FNz9DdTe6HT0mOulO5xbto+FWn1Z33lCFGa86EEsUmp1SCdlQ0Rb0DYh8eC26m0gVPIb0iTL4F7+q3+CQzyLgJsugURMCKZOvJiG/+mOnYWJK1JZcOPdQrTNjH5R3ZotqSPfYI+gdlyy1JOEcFOZ6Z84bzCA=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "18.04" - } - ] - }, - { - "component": "client", - "platform": "linux_x64", - "file": "metavms-client_update-6.0.0.38086-linux_x64-beta.zip", - "size": 281203560, - "md5": "c3be5b242d9b87574b2eb568cd43203f", - "signature": "TBLhJpLEn96Luf5ok56vbzXjOFF8ToBPlS88rGi3Wr7+Jg4oz24K0AxNSCgRcHEAwQdvja7EP1Ixgq+xvGKmIRHNIk316U7bz3ieuMOrQ36k49KsEdDlBgM+JsuUC9YqX9jx0HTVN+w97fM8iyzn7m4J3ImBIUmnUlaXgcX3T2I2Hhw5YAeQYqSiDDkOBD/GSYsHBpEiKQCL0TrBszGIMl94ADIbjZ3Dp2QPdQ7e/40GiQu9+CUAQlugGwVXf5ix0dT0K5+DS6uIKFFzIVCmX3O3JX5pf5gmC4qH35GJJRpx0IwjUk+8N55YK8uL/gGpFWOMaYelVu9Gx94rCfWrfPiww0Wfq35yhxZ7qLWPzFQmoVT4f6PZYhCjwl2lqEv8Gt5R1KpzgY3g1ZZVgBFNAVn4c08cVssUlbf2jzYS3CCEEEox747sSn3fiJRDO9lM6B2qOQVCCm4nbvG96axLUt4wpv878Jt5TGIFuitKQA7DbIKZDtFaWF0z97LnUoQo5v/oom18JzCYpxTIyw/V4CYoBwQAqpsdQrESOTr0TtnQUz07jZ8KyBkDwFEWn0DlbCPv++ZdMeHeUUlcpOhF/Pbi5RJE2PkXOV7Bv7lwo4S1hJ5/19Q39eLu2Wtj/B8ygJG8WhNPmYKs+hah5pZhyzW5fBQ7sLyrcklmkuzl+PA=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "18.04" - } - ] - }, - { - "component": "client", - "platform": "macos", - "file": "metavms-client_update-6.0.0.38086-macos-beta.zip", - "size": 219372732, - "md5": "c76a78dc11421786b0169b76c1485cf1", - "signature": "ZWk4138Gmh8qfapMFQ8nKnAZDOXlqzuhO4vZmuAYM3EeKM5s5RzmUrzoBK4n6qG/GuhjgvRP8p93LVhlgTn33Kpj7ErDUJ+f1j8Z4FPGvOO/LVBE4B7yVxbugIeatnSwcRlyhTy7s2w6llIfqPYkshAG6o344jj4i0iZsG4wEYm0jbRnpDQ545Xnl/wnJetynYRN0xGO9FfPKkuGL2ahlY9bDLOcssnPYijf7KLiAKiJ0uycVouZGnmCN/RYoID8x7WCHgjq3pFGVJ5Bx9AybOKbWndDOgWXGJSuZ8QLapM1uxM7ibsx320oBgTv+LibtqLcK5Lo+6xR5AjTSpyJj7VTkt0MNTxWiddtXKq2Vox9J/O0G6eT9SvyV0K1NPn0hFyNf2S4somSdsUko2Cs98KIhQMZLqYGQxTiS90lCzVPWor8jAagZSGWRR/0RLdstBs3eeEcdDq9pWRQpuI/E/oPbr35WZ0uNig+pw9HzhtV99UkpnJwBmytNaWOpqVEvwS3YGpHxUjsRh3pRe4JYLfeN/ytm9qCVUEjT9QrhOzrgs2ImkKuENrIoa8PLfWIG6izkTd6cIevBUR1xcbNw48cwbGfK5Ybzf9OpVWHZPvhAMTo29Lvc5P75XuhMGEezLdI18Jy1zvaYYSR7cVP0zUnyzm6frlGfg1lJSZ/K8g=", - "variants": [ - { - "minimumVersion": "10.16" - } - ] - }, - { - "component": "client", - "platform": "macos_arm64", - "file": "metavms-client_update-6.0.0.38086-macos_arm64-beta.zip", - "size": 201973015, - "md5": "ae8c852cead52283d44895cfdac4ddfa", - "signature": "GlVWMhGiC3e30/B40+yvdK8witVx9Gs3q9PgfANTg4GXYI8JvEyuKWTHnwWTZz8DRQcvUxuSBiMm8fgWucmSumXoTYMioYQPcmsiAa7GX9ZiJ93XSHEyh+2xhPSLML1U2TKKFXGjLqzTTOb/u9Fpf5j/FCqP/sHl+WtuCYxROZWpFmWRRwt9sci1l+vhlhXzNVgJ5xz76lIPfE2EVeSyQZo2u5+WEKc+Zhi/4klU3KXMrMMYXfWK6aJRTVlAYss/Aue/PMunZxAlSl5fikelTsL1aT9JOeQl7kpWNoj8GVnNTlDrtkriWYHIZIzNLCG6je3395AXYeGNjzwExwc0ufo6mLa1ITxEB6Y5LiRNP3Vo7RQstHMJLxgROt4AJgii5sU3cS3i2AJVRIxBdMJvcxWkMG9zlBKWc0WrhoJm6WhlRA3qYxbKqmulvJXG28IWiBxvr4xhxPXN5fDvrLKg03nIhclPdTRdwOxxnCIGmm6XUP5QnsoNt2N/CDbrcftNX3GaFvND4CRujKjriy8GBNSGXecLjtP4NXK1eUCewCSMVGicq4wkzc5JQuwCZf504smG4tZAioK8/bvWM6+AUnyd/xjuyjYV6tca5s+oQjMxFAvGpSR14quE1wnLRHYKj1fKrqtduZ77tWOf2qvZK3QlDgYJKtBagOULgYm8cfk=", - "variants": [ - { - "minimumVersion": "10.16" - } - ] - }, - { - "component": "client", - "platform": "macos_x64", - "file": "metavms-client_update-6.0.0.38086-macos_x64-beta.zip", - "size": 219372736, - "md5": "d646cca0af7ce95aa4dd7f90547a4438", - "signature": "ye0VzNZAdqAxD/Z66C59+XDcTL1pdZeqA+pALPZ4XXTHwhvaBDK8LG0XpAOQofwbHhmyaomSYyj4OUVmXU9OLAaJUDJjjw9/m3zXg5rlD8GnSeuThEVuDB9V4EdXLErK/QJcm9sOJd6JOjaylsXXlth3QcQrSrzKFl4VvGJDrz3uRXho3/chemd9PPLiBfXtNk9FImFP/LgayJtrGNTSXDdeKzelmO+5oH0PucHU0EuAnCV/ynvFEGfmRCyCxNQsy6fEr9+eZzFYEoWkAQkaMIOMSf1wwbQzlrfXkpqn0WO06ESuIxkhEjfkHHjM2uxMPVzhDoQkmHZ9cVEnUdjf6217lom6sq91bCki98MpzLcjwqcEwC1O/zHQg7oTAlNkwbWqWoJUedPHaFEsK1hFBBlQWn4ayWsLJpDkaKxy6nWbrpqX7kJnnTXs0kNWSm267BmtrbqqvGL/qomSOaYeJeRPwyUT8NdZ4UHk4YsANJcp2Zw4CRdpuPbzqvsUw+LYgGUzAZZ/fTJbAW36LWRjdO0r0EKaeSR0OV29lpu1P/Ed8uKKzTau8K1Ey5l+y8ErLt0B+dFL6IQDb1OSbh0ckJthH6EE/SIzEs2yLNQWzlvojHY+JwT7nsDt7b/3EMxYuvBih1Plms2fizd6ZysLkAFTcr+0+ueM18ZenVgIB0Y=", - "variants": [ - { - "minimumVersion": "10.16" - } - ] - }, - { - "component": "client", - "platform": "windows_x64", - "file": "metavms-client_update-6.0.0.38086-windows_x64-beta.zip", - "size": 199831192, - "md5": "4bcd9723ec9eb960d7afed675ede1098", - "signature": "VERbzAL23xdKFOXTs2+z8gYQPaFCoXeFX4u2DLektmGGABHd2HLvlav5KN7u6aGbkj8vPfiVr/qoA+0JyJhKoi3Y5N/fj6OUNjDDZt0FXHDPZNjxCTkCrt4k4kwVQ+OROETS8CsmtIIptz46SIaDMEvy67alUdnPcoPsy4ddcrMyJ73utwgAzCkZMBAeAfsx7g2B3xM7lX27UhhwRa+ABsZW1TciF0UgFD1WTvyKaSI3eaOLAPnZXOQlpeej2awcBN6p0Sf2HLjdHbJEishiNfHgG1MGjD5k8ZcTVItSg3hRbHWEVZ3b7dWVJIrdkwfiN+ISuUWWtqONZ10ou61Po1La08j+ro+OV/BEXsU5xP0ZT1vuH3ZW9y78Uz+bhxrDVD7zHZ+WPovGhwbt+EpiKfsZbS3jh22o6U8jhA/EL25VNwDeo19HJlwfMADrbOwoBmcPDJvyJZj5hadsfC3UBfiicfeMNH5HMP0ZEPn9rElkrlnslAU9gxcDOoL2U6uzCfNsF8OSu/DN7tqiYm7RuhbKynUJ5siGZhV+hr/Nx2WhsEdMVN8CY9yNq7FL2ysFl36eamX5qzZtnSAWkFjy1iLmqXO1Zc8HLQ/f3Rqx0w50YDML20ceK21Yus1jHpZ1jksujjDSzYsgxlGmELfXvMB4YFtrlHOV8OUglrvXad8=", - "variants": [ - { - "minimumVersion": "10.0" - } - ] - }, - { - "component": "server", - "platform": "linux_arm32", - "file": "metavms-server_update-6.0.0.38086-linux_arm32-beta.zip", - "size": 153105322, - "md5": "e985edbcd2e2f3ba3855ac87914b8a06", - "signature": "BR3SY6C3LqzM0v/Qc3RGl/SNthjU+X0bBhxEJqRzHnSh8GolOde+ttrc+5gaQGZCFjKxRVUmqXiUtHX0cmiR+qlhp5VZkjh3qG8g8TUJoL6L7dR/k5y4IPY51a517QJrGXPw+8s8HIAS4N/9HHtadzoDl/F7Z78fbkXgww4qR0vxeDDKBq8wmPN/fgnqN46CF70T75+1rIyLr6VjJ6obiBZ0vY81ZevtCOKMzqPeYgOSy+9/pOH/OuSaDdeph3i7KxaNdSr2gt883E1IBFykC524cw182+GtBvdlWefNgsHQ/2qikaxjWgx9BRr2NaoCX3qq4SrRbiGUEJ4whuRBnx1+k+iZwABghvVDnVpPsoBZc5z1wlsjoRrnWUVmFeeapdVl9sPmiRyYHOOHQqpitD2DjiOylQ/2CLOHOQlyTgLdCNOklDCvlKWI1DkR0XVeeR0y+pmOcEq97ewIRfw+A7h46+xqV4pWgxmWDnNl5ff2ociNoglqO6gh35Gc08VFE0qJFey3m/xzBLQ+iiKPTY1h2t18RFHdTt38yPtTLoFDjeUwVhFlEOS9Bn1T3Z5FOpYTkDAS3fNy+atCvK01TTChsZW69jyOpPRfCg89W88aOQ2LVHv64jUpvKE8PH9V0Ej1FzClrZO260S23SHJGjYpnMj5kvQ2fcO9lJ1J2hY=", - "variants": [ - { - "name": "debian", - "minimumVersion": "9" - }, - { - "name": "ubuntu", - "minimumVersion": "18.04" - }, - { - "name": "raspbian", - "minimumVersion": "9" - } - ] - }, - { - "component": "server", - "platform": "linux_arm64", - "file": "metavms-server_update-6.0.0.38086-linux_arm64-beta.zip", - "size": 128927202, - "md5": "e9728af6304a1380a706afa9d16eab21", - "signature": "dbQVDOUDfD8d01XNHP0N/X3XlUaO2/LWGPy/LQys9ZTMS+2XGABOfDDKWsnwSKGuprBasbJKwQxu7bsGhkvmijqhRK0/fVWc8Rcdjo4cle16YiVX6kVm1O+zWH7cI2kqySSaGwsP1HsbYl6+4NzUm3eh0RjXb2IaErZHIRfZKvhNmQmihBAFXjEYMtO5yUDnsa6WjAVswqbqoLacjSEN2DgFDLnE8HcQZQBQF4mEN0tLidFF2TEWiRp6w5EDobO8ljhi9MvCtubqHjvQJJCkayZ1k68u75jjFWV4mn2oCK6PHbL45BDT123c80xfh/EIoLbfq08MHcb0W7yvFZp5EBsxqOOOPgmgxBsEMp4tkxcjEngXSiYuyqtC9aeuKYVD2obx5KXn/7wDD2y5a8KnPk97nGw+4bXSnqYmai8t1oXVbWb0VUrVgql4H/onejvROqKuE5EV5Yh4pvxXSbv32odvTL/0dd7o/o3YSuSpyYCAyPeGfT6CJ6TuBipm0QVgFLTRWtYvAEo6O8jU/bLEwdcDIxbVw/b0SXJieQF9buTIEoEIdXKqG7P/yZjJmyw8cLZuwgZftZYi7kI+JxfARsqYXoWGNTUo/X+DuPYDlS92dELHNCR55oMYimwK2Dn4FOIBJ76IEhTEeaihESamOqNnao0ujhyZuafLJhJIQG4=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "18.04" - } - ] - }, - { - "component": "server", - "platform": "linux_arm64", - "file": "metavms-server_update-6.0.0.38086-linux_arm64.vivotek_edge1-beta.zip", - "size": 178921404, - "md5": "5e670d1ae52ed6e1c06974698d6e3117", - "signature": "I/6i8dac7k5QBOKJS6+qhIJDPFD1SU/6igv7T6gWuzWgWBD7EtUJOUqG5PkagnJT698AUYV3fitFgcikxOoqq/31tdiZq3Ig94NHVrwqKKmjnMkWoLVWpGLS89MnDXeJXMqXYTCBEkYyU7OyD7VynjyRW+RcC1AK5gfrB1OXH4tRFJ7USxG8ZTcagMjTX9dibSWOy1Lhb1fypkYU92sgIg0+oH/TqucS7IdA9EcGzgYEmbQB8RcDnI8pmGdqaXSRzSeOTmjIY3xHKxhzHQKwz76jphb/J7dy+b14N4ogLwexG1S8TdVbkVPKVZzHo3PRLHlC1A9AyDC0NGKo/MOVcrqdPz39vRFnI8Xt+NqJAYGdIejjiuZjupBv7BLX1yHl8z/5OryX6Idhs2w7c77//MVXlpLoVVTzAQH069ECy6ZX4olia323R/3s/PTRIOzlvZfWXe7oPqBVQYA/CcuikEgHPvTd/1780bKLExQ4uE+JFvSU1NZ6+dfMX5uKIGaMfpKSo/fj6D9WtHUpxIiSmgE8urHrkEqimXLuTIc9OO3OtQufNxNiQtcGvSdf+MXsWwoGXAW4IwwO1mF+wSkxQUXHWBJO5iHHuAAlJDqIlCVwbEVZD6Zb+5L3xP0yeNSqY5tLC/ak3mfwbRb1V37gJvLJ0Xip1u+erXrtDM9pwvU=", - "variants": [ - { - "name": "flavor:vivotek_edge1" - } - ] - }, - { - "component": "server", - "platform": "linux_x64", - "file": "metavms-server_update-6.0.0.38086-linux_x64-beta.zip", - "size": 114637378, - "md5": "3cbce22ff5a59849b4faf23c2112090c", - "signature": "fORSN39H2h6QQVnlAIOy2kjBPazWoK+sHA523yb4gNFOoLFQsQa6E/O+kqC8jjpPJMlu00Cg8elHO83rAKXvD1klSym2ByAvHNjBU4xH18S76FsmtZrhRjG0MovL/D8M2Z3g/50Y7YPIBAKn/ughjAuYd27zznWK3REbkkkKXYMROXE1dMKGAzAbeVGIvOOZRe5iGMGQI2L9u8mtFsZoGFlZerB8bkYOF5rdN+KBrNPslChW66579t0KyqDo7CjmE8x3imc0HW0hMd5gdDQ8DIRsNXzbjLRIqtuiDzCCfIb8eNFTMftKfhp7EKtBAdR2yLJz09PkdSMr8ri2SxZTClb77zl59ewZE2lnjg8Z8AJINTe3q22zdzO4TkGrYLvk8l3LvLFCYts44IjfgEQuCLmYJe+4HVn/lT5/XReqSUBHbmiqbcnvteljt/Q7e2TbW7YH7K3mfCFojsQJbQTIrLQKqTSvasMsayLZrLoIa9ZZBpShj54PPYCvCiSE93fhIh8kXaNXKQK1QR9c/c+GUwd5DHbFVUzrWYpnaiJs0btmA8MKvxFx27AGX7eTlHzHGmNSxflK7mDmycGkM7FAJJfGM8Q0AeLLt9qTq+eaqWwo4QJRlJYvxAGTvGSNOfj6cY/Wwz7hRljfaF4/gwodudT34UWkDFy36xh6utVLNuI=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "18.04" - } - ] - }, - { - "component": "server", - "platform": "windows_x64", - "file": "metavms-server_update-6.0.0.38086-windows_x64-beta.zip", - "size": 109881758, - "md5": "efabdca9e0d37bb8fc00475c1333b4ac", - "signature": "ACffywPrAIX8hzdA62P+giSEoiB+Q/0nOe59pLn6ivoG3osrTzpv7YLKPiB9w5GZ0DPyExLHRkp/98wAUw7rGkYsezFjzynA0/r5hYURj26ew7dGVz0sg+NI3adSj45o+d6rhsMao39A2cU8eTjcLm9+cgjaJFtE7EmJHv5TymNNDHKpteNIsGzNgIiZrRyUA7YM/9IUMEWEkzAe3LBygHx76V/3lBZU50Wb/pxUxA0+WuImT9uwLQWDCtG/xdHgCt9+FfjpWLCN18G9vP4QGVnEEOgXqfv6nS2FID6ylal8oCbcbmz3P6iNJHMIryztQDXkZTQ9bjCrCro+5RPKJ9NgxcxeKzuL7SglgW/uiBQ6mwLRAHQRsQidw0LLfwLKWnKfk5LjIMrOBIVfCZfQHqntDSBd0B24lq3RRILlUthrULaqn0wXv7N8BkOafSNeKICCKJb7SL4OTbpmnemc7R7y/bpVGXjTkCBq2ROn2Ank3czq2X/RdkpVWFzjC6U+eKZ2pDU5kV3EvYWQMcydTumASHo461so9axTxRlhmVx86RM7aEBVFpGgs3Jmh5+pDdLwAFVgoq6QKqLGONcaIO3GtAsD28VZL8uXZOmNf0Zd8bh1IpSoSyptQnNvIKefyVZyyaXzCKGHHbLGa66g42VKY9kdBVUM9N03mQT9Zyo=", - "variants": [ - { - "minimumVersion": "10.0" - } - ] - } - ] +{ + "version": "6.0.0.38086", + "cloudHost": "meta.nxvms.com", + "releaseNotesUrl": "https://meta.nxvms.com/downloads/releases", + "description": "", + "eula": "\n\n\n \n Nx Witness EULA\n\n\n\t

NETWORK OPTIX, INC.

\n

END USER LICENSE AGREEMENT

\n

IMPORTANT – PLEASE READ CAREFULLY: This End User License Agreement (the “Agreement”) is a legally enforceable contract between You (either an individual or an entity) and Network Optix, Inc. (the “Company”) regarding the use of the Company’s software programs accompanying or provided under this Agreement together with the SDK made available by the Company, associated media, corresponding Documentation, technical configurations, and technical data (collectively, the “Software”). “Documentation” means the user’s guide and other documentation (including print and online), if any, provided to you with the Software.

\n

BY CLICKING “I ACCEPT” BUTTON BELOW OR BY DOWNLOADING, INSTALLING OR OTHERWISE USING THE SOFTWARE, YOU CONSENT TO BE LEGALLY BOUND BY ALL THE TERMS AND CONDITIONS OF THE AGREEMENT. IF YOU ARE ENTERING INTO THE AGREEMENT ON BEHALF OF A COMPANY OR OTHER LEGAL ENTITY, YOU REPRESENT THAT YOU HAVE THE AUTHORITY TO BIND SUCH ENTITY AND ITS AFFILIATES TO THE AGREEMENT, IN WHICH CASE THE TERMS \"YOU\" OR \"YOUR\" SHALL REFER TO SUCH ENTITY AND ITS AFFILIATES. IF YOU DO NOT HAVE SUCH AUTHORITY, OR IF YOU DO NOT AGREE WITH ANY OF THE TERMS OF THE AGREEMENT, YOU MUST NOT ACCEPT THE AGREEMENT AND MAY NOT USE THE SOFTWARE.

\n

In addition, certain third party programs or code (“Third Party Software”) are being provided to you under the terms and conditions of a separate end user license accompanying such Third Party Software (“Third Party Software Licenses”), which will be provided to you by the Company as part of the Documentation or may be made available for viewing by the Company at the website or webpage at https://meta.nxvms.com/content/libraries (the “Third Party Site”). If required by any Third Party Software License, the Company will make such Third Party Software, and the Company’s modifications to that Third Party Software, available to you on the Third Party Site. The Company may be required or wish to provide you with certain notices or information about Third Party Software or Third Party Software Licenses and you acknowledge that such notices and information may be provided to you by the posting of such notices and information on the Third Party Site.. BY INSTALLING, COPYING OR OTHERWISE USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS AGREEMENT AS WELL AS THE TERMS OF THIRD PARTY SOFTWARE LICENSES. IF YOU DO NOT AGREE TO ALL SUCH TERMS, DO NOT INSTALL OR USE THE SOFTWARE.

\n

1. License Grant. Subject to the terms and conditions of this Agreement, the Company grants you a non-exclusive, non-transferable, non-sublicensable, limited license to install and use the Software, in object code version only, for your own use subject to the restrictions contained in this Agreement.

\n

2. Backup Copies. You may make one backup copy of the Software, provided your backup copy is not installed or used on any computer and further provided that all such copies shall bear the original and unmodified copyright, patent and other intellectual property markings that appear on or in the Software. You may not transfer the rights to a backup copy unless you transfer all rights in the Software as provided in this Agreement.

\n

3. Ownership. You acknowledge that the Software and any copies that you are authorized by the Company to make are the intellectual property of, and are owned by, the Company and its licensors. The structure, sequence and organization of the Software are the valuable trade secrets and confidential information of the Company and its licensors. The Software is protected by copyright, including without limitation by United States copyright law, international treaty provisions and applicable laws in the country in which it is used. You acknowledge that the Company, or its licensors, retain the ownership of all patents, copyrights, trade secrets, trademarks and other intellectual property rights pertaining to the Software. You will take no actions which adversely affect the Company’s intellectual property rights in the Software. This Agreement is a license and not an agreement for sale. No title to, or ownership of, the Software, or any intellectual property rights subsisting therein, is transferred to you.

\n

4. Restrictions. The Software is licensed solely for your own use. The Company and its licensors reserve all rights not expressly granted to you in this Agreement. Without limiting the generality of the foregoing, you shall not nor shall you permit any other party to: (i) make copies of the Software except as expressly set forth in this Agreement, or (ii) disassemble, decompile or translate any part of the Software, or otherwise attempt to reconstruct or discover the source code of the Software, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding this limitation, or (iii) modify or create derivative works based upon the Software, or (iv) externally distribute, sublicense, resell, encumber or otherwise transfer the Software, or (v) rent, lease, lend, or use the Software, or any part thereof, for timesharing or bureau use, or (vi) allow a third party to copy, access, or use the Software (except as expressly provided in this Agreement), or (vi) alter or remove any copyright, trademark or other proprietary notice which may appear on the Software, (vii) take any action that would cause the Software to be placed in the public domain or become subject to open source license agreement, or (viii) use the Software in any manner that violates any statute, law, rule, regulation, directive, guideline, bylaw whether presently in force or may be implemented by federal, state or local authorities.

\n

5. Paid Features. The Software is made available to you at no charge or fees. However, the Company may offer certain premium features to you upon payment of a license fees determined by the Company in its sole discretion (“Paid Features”). In order to use Paid Features, you must agree to pay the license fees plus any applicable taxes through one of the payment methods accepted by the Company. Upon receipt of the license fees, the Company will either provide you a license key to activate the Paid Features or allow you to download the Software with the Paid Features. Activation of Paid Features may also be regulated through the use of transferrable license keys, and such license keys may only be transferred within your organization subject to the limitations listed in your purchase order or other documentation describing the Software and/or license rights obtained in the Software. You expressly acknowledge and agree that this Agreement will govern your use of the Paid Features.

\n

6. Updates. Except as otherwise expressly provided herein, this Agreement will govern any updates and upgrades to the Software that may be provided to you in accordance with the Company’s then-current maintenance and support policies, unless such updates and upgrades are provided under a separate license agreement. You may use such updates or upgrades only in conjunction with your then-existing the Software licensed under this Agreement. The Software and all updates and upgrades are licensed as a single product and such updates and upgrades may not be separated from the Software to exceed the scope of your original license. Any other software that may be provided with the Software that is associated with a separate end-user license agreement is licensed to you under the terms of that separate license agreement.

\n

7. No Support. Except as expressly required by applicable law, nothing in this Agreement entitles you to any support, maintenance or new versions of the Software. You may contact the Company to determine the availability of support, maintenance, and new versions, and the fees terms and conditions that would apply.

\n

8. Disclaimer of Warranties. THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OR REPRESENTATION OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. NEITHER THE COMPANY NOR ITS LICENSORS REPRESENT OR WARRANT THAT THE SOFTWARE WILL SATISFY YOUR REQUIREMENTS OR THAT IT IS WITHOUT DEFECT OR ERROR OR THAT THE OPERATION THEREOF WILL BE UNINTERRUPTED OR ERROR FREE. The Company does not warrant that functions contained in the Software will meet your requirements or that the operation of the Software will be error-free or uninterrupted. The Company is not responsible for problems caused by changes in, or modifications to, the operating characteristics of any computer hardware or operating system for which the Software or any upgrade or update is procured, nor is the Company responsible for problems which occur as a result of the use of the Software in conjunction with software of third parties or with hardware, which does not meet the Company’s specific requirements. Some jurisdictions do not allow certain disclaimers or limitations of warranties, so some of these may not apply to you.

\n

9. Limitation Of Liability. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW EVEN IF A REMEDY FAILS ITS ESSENTIAL PURPOSE, IN NO EVENT SHALL THE COMPANY OR ITS LICENSORS BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE OR THE PROVISION OF OR FAILURE TO PROVIDE SUPPORT SERVICES, EVEN IF THE COMPANY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN ANY CASE, THE COMPANY’S ENTIRE LIABILITY TO YOU OR ANY OTHER PARTY FOR LOSS OR DAMAGE RESULTING FROM ANY CLAIMS, DEMANDS OR ACTIONS ARISING OUT OF THIS AGREEMENT SHALL NOT EXCEED THE FEE ACTUALLY PAID BY YOU TO THE COMPANY FOR THE SOFTWARE, IF ANY, NOTWITHSTANDING ANY FAILURE OF ESSENTIAL PURPOSE OF ANY LIMITED REMEDY. Because some states and jurisdictions do not allow the exclusion or limitation of liability, the above limitation may not apply to you. In such states and jurisdictions, the Company’s liability shall be limited to the greatest extent permitted by law.

\n

10. Sole Remedy and Allocation of Risk. YOUR SOLE AND EXCLUSIVE REMEDY AND THE COMPANY’S SOLE AND EXCLUSIVE LIABILITY ARE SET FORTH IN THIS AGREEMENT. This Agreement defines a mutually agreed-upon allocation of risk.

\n

11. Third Party Software Licenses. Third Party Software included with the Software is licensed under the terms of Third Party Software Licenses. You understand and agree that, although provided to you by the Company with the Software, your use of Third Party Software shall be and is governed by such Third Party Software Licenses. You understand and acknowledge that Third Party Software is being provided “AS IS” WITHOUT ANY WARRANTY, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. If there is a conflict between this Agreement and any Third Party Software License with respect to Third Party Software, the provisions of such Third Party Software License shall prevail.

\n

12. Export Restrictions. You agree to comply with all export and re-export restrictions and regulations (“Export Restrictions”) imposed by the United States or any other country in which you conduct business. Without limiting the generality of the foregoing, and regardless of any disclosure made by you to the Company regarding an ultimate destination of the Software, you represent and warrant that you understand the U.S. law currently prohibits the export or re-export, directly or indirectly (including via remote access) of U.S. origin products and technology to certain proscribed countries (including Cuba, Iran, Sudan, North Korea and Syria and/or any other country that may become subject to an embargo by the United States), entities, organizations and individuals, without prior authorization from the U.S. Government. You will not commit any act or omission that will result in a breach of any such Export Restrictions. Your breach of this clause shall constitute cause for immediate termination of this Agreement.

\n

13. High Risk Activities. The Software is not designed, manufactured or intended for use or resale as on-line control equipment in hazardous environments requiring fail-safe performance, such as in the operation of nuclear facilities, aircraft navigation or communication systems, air traffic control, direct life support machines, or weapons systems, in which the failure of the Software could lead directly to death, personal injury, or severe physical or environmental damage (“High Risk Activities”). The Company specifically disclaims any express or implied warranty of fitness for High Risk Activities.

\n

14. Termination. The licenses granted herein shall automatically terminate without notice if you fail to comply with any material provision of this Agreement. In such event, you must immediately uninstall and return the Software, including all Documentation and original media, to the Company. This shall not limit or affect any remedy available to the Company for your breach of this Agreement.

\n

15. Miscellaneous.

\n

(a) If you are a unit or agency of the United States Government, the following applies: The Software is provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the Government is subject to restrictions as set forth in Subparagraphs (a) through (d) of the Commercial Computer-Restricted Rights clause at FAR 52.227-19 when applicable, or in Subparagraph 252.227-7013 (c)(1)(ii) of the Rights in Technical Data and Computer Software at DFARS, and in similar clauses in the NASA FAR Supplement. Contractor/manufacturer is Network Optix, Inc.

\n

(b) This Agreement shall be governed by the laws of the State of California, exclusive of its choice of law rules. Any dispute between you and the Company regarding this Agreement will be subject to the exclusive jurisdiction of the state and federal courts sitting in the County of Los Angeles in the State of California. In any such action, the prevailing party will be entitled to costs and attorneys’ fees.

\n

(c) You may not assign, sublicense, or transfer this Agreement, the Software, or any rights or obligations hereunder without prior written consent of the Company. Any such attempted assignment, sublicense, or transfer will be null and void. The Company may terminate this Agreement in the event of any such attempted assignment, sublicense, or transfer.

\n

(d) This Agreement is in the English language only, and will only be provided in the English language, which language will be controlling in all respects, and all versions hereof in any other language will not be binding on the parties hereto. All communications and notices to be made or given pursuant to this Agreement must be in the English language.

\n

(e) The Agreement constitutes the final and complete understanding between you and the Company with respect to the subject matter of this Agreement. Any modifications or waivers of this Agreement must be in writing and signed by both parties hereto.

\n

(f) If any provision in this Agreement is invalid or unenforceable or contrary to applicable law, such provision shall be construed, limited, or altered, as necessary, to eliminate the invalidity or unenforceability or the conflict with applicable law, and all other provisions of this Agreement shall remain in effect.

\n

(g) Failure or delay on the part of the Company to exercise any right, power, privilege, or remedy will not constitute a waiver of, or bar the later exercise of, that or any other right, power, privilege, or remedy of the Company.

\n

(h) No rule of strict construction shall apply against or in favor of either party in the construction and interpretation of this Agreement.

\n

(i) This document was originally written in the English language, and the English text of this document shall prevail over any translation hereof.

\n\n", + "eulaVersion": 1, + "packages": [ + { + "component": "client", + "platform": "linux_arm64", + "file": "metavms-client_update-6.0.0.38086-linux_arm64-beta.zip", + "size": 282726618, + "md5": "79fb0e0d413f76fd888b9f1b69018b46", + "signature": "g7V7D7N7K09qTq0f1h2KRPK3El+HGUbYD7sTw0cw5ZcSPfWqchjFoqmQwseJTvyvjsmefW8dL/XeQTp6zrKJf/6+fA9j7BQoXz6IXWme8m5reob9ZnEhOuCrHNi2zf3JO9+PCwrVAkV2AVlIMMTr49gmAPvVn9c0O/jilqz6RtaZm5CWYjrdXUyPIbqer/aNxKpNNLfHJ4GkBEospoRpLqofxBXM1C2V9/R9tzqMoDO9R81fJjnztrqyoBRl1BdOPxTojNQ7D7AgAxOnsE8nSaq0HIn2ZvBmTY9q7OCv28jk2/6zUwKYoebw9SAbKcMa8O/nqZtQwU7EtGI0GKOhMJ1wUzKb2ymuhPrVrfeMhoB4FPjTfy2aUIfVDr7FgHePaxSpywowYcApdd406bW4kUoyp24dcz5NgTKoZJiMbubRWVpeB5DKcSFKgWjPE8Emaw5LCwJ+urlEpO8kcwaUDeBJAeB1VHYbknfoHVyWZtld98hzkhqkHQDCVl+gfHlx2BPEkAf4HdOIYN6FNz9DdTe6HT0mOulO5xbto+FWn1Z33lCFGa86EEsUmp1SCdlQ0Rb0DYh8eC26m0gVPIb0iTL4F7+q3+CQzyLgJsugURMCKZOvJiG/+mOnYWJK1JZcOPdQrTNjH5R3ZotqSPfYI+gdlyy1JOEcFOZ6Z84bzCA=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "18.04" + } + ] + }, + { + "component": "client", + "platform": "linux_x64", + "file": "metavms-client_update-6.0.0.38086-linux_x64-beta.zip", + "size": 281203560, + "md5": "c3be5b242d9b87574b2eb568cd43203f", + "signature": "TBLhJpLEn96Luf5ok56vbzXjOFF8ToBPlS88rGi3Wr7+Jg4oz24K0AxNSCgRcHEAwQdvja7EP1Ixgq+xvGKmIRHNIk316U7bz3ieuMOrQ36k49KsEdDlBgM+JsuUC9YqX9jx0HTVN+w97fM8iyzn7m4J3ImBIUmnUlaXgcX3T2I2Hhw5YAeQYqSiDDkOBD/GSYsHBpEiKQCL0TrBszGIMl94ADIbjZ3Dp2QPdQ7e/40GiQu9+CUAQlugGwVXf5ix0dT0K5+DS6uIKFFzIVCmX3O3JX5pf5gmC4qH35GJJRpx0IwjUk+8N55YK8uL/gGpFWOMaYelVu9Gx94rCfWrfPiww0Wfq35yhxZ7qLWPzFQmoVT4f6PZYhCjwl2lqEv8Gt5R1KpzgY3g1ZZVgBFNAVn4c08cVssUlbf2jzYS3CCEEEox747sSn3fiJRDO9lM6B2qOQVCCm4nbvG96axLUt4wpv878Jt5TGIFuitKQA7DbIKZDtFaWF0z97LnUoQo5v/oom18JzCYpxTIyw/V4CYoBwQAqpsdQrESOTr0TtnQUz07jZ8KyBkDwFEWn0DlbCPv++ZdMeHeUUlcpOhF/Pbi5RJE2PkXOV7Bv7lwo4S1hJ5/19Q39eLu2Wtj/B8ygJG8WhNPmYKs+hah5pZhyzW5fBQ7sLyrcklmkuzl+PA=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "18.04" + } + ] + }, + { + "component": "client", + "platform": "macos", + "file": "metavms-client_update-6.0.0.38086-macos-beta.zip", + "size": 219372732, + "md5": "c76a78dc11421786b0169b76c1485cf1", + "signature": "ZWk4138Gmh8qfapMFQ8nKnAZDOXlqzuhO4vZmuAYM3EeKM5s5RzmUrzoBK4n6qG/GuhjgvRP8p93LVhlgTn33Kpj7ErDUJ+f1j8Z4FPGvOO/LVBE4B7yVxbugIeatnSwcRlyhTy7s2w6llIfqPYkshAG6o344jj4i0iZsG4wEYm0jbRnpDQ545Xnl/wnJetynYRN0xGO9FfPKkuGL2ahlY9bDLOcssnPYijf7KLiAKiJ0uycVouZGnmCN/RYoID8x7WCHgjq3pFGVJ5Bx9AybOKbWndDOgWXGJSuZ8QLapM1uxM7ibsx320oBgTv+LibtqLcK5Lo+6xR5AjTSpyJj7VTkt0MNTxWiddtXKq2Vox9J/O0G6eT9SvyV0K1NPn0hFyNf2S4somSdsUko2Cs98KIhQMZLqYGQxTiS90lCzVPWor8jAagZSGWRR/0RLdstBs3eeEcdDq9pWRQpuI/E/oPbr35WZ0uNig+pw9HzhtV99UkpnJwBmytNaWOpqVEvwS3YGpHxUjsRh3pRe4JYLfeN/ytm9qCVUEjT9QrhOzrgs2ImkKuENrIoa8PLfWIG6izkTd6cIevBUR1xcbNw48cwbGfK5Ybzf9OpVWHZPvhAMTo29Lvc5P75XuhMGEezLdI18Jy1zvaYYSR7cVP0zUnyzm6frlGfg1lJSZ/K8g=", + "variants": [ + { + "minimumVersion": "10.16" + } + ] + }, + { + "component": "client", + "platform": "macos_arm64", + "file": "metavms-client_update-6.0.0.38086-macos_arm64-beta.zip", + "size": 201973015, + "md5": "ae8c852cead52283d44895cfdac4ddfa", + "signature": "GlVWMhGiC3e30/B40+yvdK8witVx9Gs3q9PgfANTg4GXYI8JvEyuKWTHnwWTZz8DRQcvUxuSBiMm8fgWucmSumXoTYMioYQPcmsiAa7GX9ZiJ93XSHEyh+2xhPSLML1U2TKKFXGjLqzTTOb/u9Fpf5j/FCqP/sHl+WtuCYxROZWpFmWRRwt9sci1l+vhlhXzNVgJ5xz76lIPfE2EVeSyQZo2u5+WEKc+Zhi/4klU3KXMrMMYXfWK6aJRTVlAYss/Aue/PMunZxAlSl5fikelTsL1aT9JOeQl7kpWNoj8GVnNTlDrtkriWYHIZIzNLCG6je3395AXYeGNjzwExwc0ufo6mLa1ITxEB6Y5LiRNP3Vo7RQstHMJLxgROt4AJgii5sU3cS3i2AJVRIxBdMJvcxWkMG9zlBKWc0WrhoJm6WhlRA3qYxbKqmulvJXG28IWiBxvr4xhxPXN5fDvrLKg03nIhclPdTRdwOxxnCIGmm6XUP5QnsoNt2N/CDbrcftNX3GaFvND4CRujKjriy8GBNSGXecLjtP4NXK1eUCewCSMVGicq4wkzc5JQuwCZf504smG4tZAioK8/bvWM6+AUnyd/xjuyjYV6tca5s+oQjMxFAvGpSR14quE1wnLRHYKj1fKrqtduZ77tWOf2qvZK3QlDgYJKtBagOULgYm8cfk=", + "variants": [ + { + "minimumVersion": "10.16" + } + ] + }, + { + "component": "client", + "platform": "macos_x64", + "file": "metavms-client_update-6.0.0.38086-macos_x64-beta.zip", + "size": 219372736, + "md5": "d646cca0af7ce95aa4dd7f90547a4438", + "signature": "ye0VzNZAdqAxD/Z66C59+XDcTL1pdZeqA+pALPZ4XXTHwhvaBDK8LG0XpAOQofwbHhmyaomSYyj4OUVmXU9OLAaJUDJjjw9/m3zXg5rlD8GnSeuThEVuDB9V4EdXLErK/QJcm9sOJd6JOjaylsXXlth3QcQrSrzKFl4VvGJDrz3uRXho3/chemd9PPLiBfXtNk9FImFP/LgayJtrGNTSXDdeKzelmO+5oH0PucHU0EuAnCV/ynvFEGfmRCyCxNQsy6fEr9+eZzFYEoWkAQkaMIOMSf1wwbQzlrfXkpqn0WO06ESuIxkhEjfkHHjM2uxMPVzhDoQkmHZ9cVEnUdjf6217lom6sq91bCki98MpzLcjwqcEwC1O/zHQg7oTAlNkwbWqWoJUedPHaFEsK1hFBBlQWn4ayWsLJpDkaKxy6nWbrpqX7kJnnTXs0kNWSm267BmtrbqqvGL/qomSOaYeJeRPwyUT8NdZ4UHk4YsANJcp2Zw4CRdpuPbzqvsUw+LYgGUzAZZ/fTJbAW36LWRjdO0r0EKaeSR0OV29lpu1P/Ed8uKKzTau8K1Ey5l+y8ErLt0B+dFL6IQDb1OSbh0ckJthH6EE/SIzEs2yLNQWzlvojHY+JwT7nsDt7b/3EMxYuvBih1Plms2fizd6ZysLkAFTcr+0+ueM18ZenVgIB0Y=", + "variants": [ + { + "minimumVersion": "10.16" + } + ] + }, + { + "component": "client", + "platform": "windows_x64", + "file": "metavms-client_update-6.0.0.38086-windows_x64-beta.zip", + "size": 199831192, + "md5": "4bcd9723ec9eb960d7afed675ede1098", + "signature": "VERbzAL23xdKFOXTs2+z8gYQPaFCoXeFX4u2DLektmGGABHd2HLvlav5KN7u6aGbkj8vPfiVr/qoA+0JyJhKoi3Y5N/fj6OUNjDDZt0FXHDPZNjxCTkCrt4k4kwVQ+OROETS8CsmtIIptz46SIaDMEvy67alUdnPcoPsy4ddcrMyJ73utwgAzCkZMBAeAfsx7g2B3xM7lX27UhhwRa+ABsZW1TciF0UgFD1WTvyKaSI3eaOLAPnZXOQlpeej2awcBN6p0Sf2HLjdHbJEishiNfHgG1MGjD5k8ZcTVItSg3hRbHWEVZ3b7dWVJIrdkwfiN+ISuUWWtqONZ10ou61Po1La08j+ro+OV/BEXsU5xP0ZT1vuH3ZW9y78Uz+bhxrDVD7zHZ+WPovGhwbt+EpiKfsZbS3jh22o6U8jhA/EL25VNwDeo19HJlwfMADrbOwoBmcPDJvyJZj5hadsfC3UBfiicfeMNH5HMP0ZEPn9rElkrlnslAU9gxcDOoL2U6uzCfNsF8OSu/DN7tqiYm7RuhbKynUJ5siGZhV+hr/Nx2WhsEdMVN8CY9yNq7FL2ysFl36eamX5qzZtnSAWkFjy1iLmqXO1Zc8HLQ/f3Rqx0w50YDML20ceK21Yus1jHpZ1jksujjDSzYsgxlGmELfXvMB4YFtrlHOV8OUglrvXad8=", + "variants": [ + { + "minimumVersion": "10.0" + } + ] + }, + { + "component": "server", + "platform": "linux_arm32", + "file": "metavms-server_update-6.0.0.38086-linux_arm32-beta.zip", + "size": 153105322, + "md5": "e985edbcd2e2f3ba3855ac87914b8a06", + "signature": "BR3SY6C3LqzM0v/Qc3RGl/SNthjU+X0bBhxEJqRzHnSh8GolOde+ttrc+5gaQGZCFjKxRVUmqXiUtHX0cmiR+qlhp5VZkjh3qG8g8TUJoL6L7dR/k5y4IPY51a517QJrGXPw+8s8HIAS4N/9HHtadzoDl/F7Z78fbkXgww4qR0vxeDDKBq8wmPN/fgnqN46CF70T75+1rIyLr6VjJ6obiBZ0vY81ZevtCOKMzqPeYgOSy+9/pOH/OuSaDdeph3i7KxaNdSr2gt883E1IBFykC524cw182+GtBvdlWefNgsHQ/2qikaxjWgx9BRr2NaoCX3qq4SrRbiGUEJ4whuRBnx1+k+iZwABghvVDnVpPsoBZc5z1wlsjoRrnWUVmFeeapdVl9sPmiRyYHOOHQqpitD2DjiOylQ/2CLOHOQlyTgLdCNOklDCvlKWI1DkR0XVeeR0y+pmOcEq97ewIRfw+A7h46+xqV4pWgxmWDnNl5ff2ociNoglqO6gh35Gc08VFE0qJFey3m/xzBLQ+iiKPTY1h2t18RFHdTt38yPtTLoFDjeUwVhFlEOS9Bn1T3Z5FOpYTkDAS3fNy+atCvK01TTChsZW69jyOpPRfCg89W88aOQ2LVHv64jUpvKE8PH9V0Ej1FzClrZO260S23SHJGjYpnMj5kvQ2fcO9lJ1J2hY=", + "variants": [ + { + "name": "debian", + "minimumVersion": "9" + }, + { + "name": "ubuntu", + "minimumVersion": "18.04" + }, + { + "name": "raspbian", + "minimumVersion": "9" + } + ] + }, + { + "component": "server", + "platform": "linux_arm64", + "file": "metavms-server_update-6.0.0.38086-linux_arm64-beta.zip", + "size": 128927202, + "md5": "e9728af6304a1380a706afa9d16eab21", + "signature": "dbQVDOUDfD8d01XNHP0N/X3XlUaO2/LWGPy/LQys9ZTMS+2XGABOfDDKWsnwSKGuprBasbJKwQxu7bsGhkvmijqhRK0/fVWc8Rcdjo4cle16YiVX6kVm1O+zWH7cI2kqySSaGwsP1HsbYl6+4NzUm3eh0RjXb2IaErZHIRfZKvhNmQmihBAFXjEYMtO5yUDnsa6WjAVswqbqoLacjSEN2DgFDLnE8HcQZQBQF4mEN0tLidFF2TEWiRp6w5EDobO8ljhi9MvCtubqHjvQJJCkayZ1k68u75jjFWV4mn2oCK6PHbL45BDT123c80xfh/EIoLbfq08MHcb0W7yvFZp5EBsxqOOOPgmgxBsEMp4tkxcjEngXSiYuyqtC9aeuKYVD2obx5KXn/7wDD2y5a8KnPk97nGw+4bXSnqYmai8t1oXVbWb0VUrVgql4H/onejvROqKuE5EV5Yh4pvxXSbv32odvTL/0dd7o/o3YSuSpyYCAyPeGfT6CJ6TuBipm0QVgFLTRWtYvAEo6O8jU/bLEwdcDIxbVw/b0SXJieQF9buTIEoEIdXKqG7P/yZjJmyw8cLZuwgZftZYi7kI+JxfARsqYXoWGNTUo/X+DuPYDlS92dELHNCR55oMYimwK2Dn4FOIBJ76IEhTEeaihESamOqNnao0ujhyZuafLJhJIQG4=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "18.04" + } + ] + }, + { + "component": "server", + "platform": "linux_arm64", + "file": "metavms-server_update-6.0.0.38086-linux_arm64.vivotek_edge1-beta.zip", + "size": 178921404, + "md5": "5e670d1ae52ed6e1c06974698d6e3117", + "signature": "I/6i8dac7k5QBOKJS6+qhIJDPFD1SU/6igv7T6gWuzWgWBD7EtUJOUqG5PkagnJT698AUYV3fitFgcikxOoqq/31tdiZq3Ig94NHVrwqKKmjnMkWoLVWpGLS89MnDXeJXMqXYTCBEkYyU7OyD7VynjyRW+RcC1AK5gfrB1OXH4tRFJ7USxG8ZTcagMjTX9dibSWOy1Lhb1fypkYU92sgIg0+oH/TqucS7IdA9EcGzgYEmbQB8RcDnI8pmGdqaXSRzSeOTmjIY3xHKxhzHQKwz76jphb/J7dy+b14N4ogLwexG1S8TdVbkVPKVZzHo3PRLHlC1A9AyDC0NGKo/MOVcrqdPz39vRFnI8Xt+NqJAYGdIejjiuZjupBv7BLX1yHl8z/5OryX6Idhs2w7c77//MVXlpLoVVTzAQH069ECy6ZX4olia323R/3s/PTRIOzlvZfWXe7oPqBVQYA/CcuikEgHPvTd/1780bKLExQ4uE+JFvSU1NZ6+dfMX5uKIGaMfpKSo/fj6D9WtHUpxIiSmgE8urHrkEqimXLuTIc9OO3OtQufNxNiQtcGvSdf+MXsWwoGXAW4IwwO1mF+wSkxQUXHWBJO5iHHuAAlJDqIlCVwbEVZD6Zb+5L3xP0yeNSqY5tLC/ak3mfwbRb1V37gJvLJ0Xip1u+erXrtDM9pwvU=", + "variants": [ + { + "name": "flavor:vivotek_edge1" + } + ] + }, + { + "component": "server", + "platform": "linux_x64", + "file": "metavms-server_update-6.0.0.38086-linux_x64-beta.zip", + "size": 114637378, + "md5": "3cbce22ff5a59849b4faf23c2112090c", + "signature": "fORSN39H2h6QQVnlAIOy2kjBPazWoK+sHA523yb4gNFOoLFQsQa6E/O+kqC8jjpPJMlu00Cg8elHO83rAKXvD1klSym2ByAvHNjBU4xH18S76FsmtZrhRjG0MovL/D8M2Z3g/50Y7YPIBAKn/ughjAuYd27zznWK3REbkkkKXYMROXE1dMKGAzAbeVGIvOOZRe5iGMGQI2L9u8mtFsZoGFlZerB8bkYOF5rdN+KBrNPslChW66579t0KyqDo7CjmE8x3imc0HW0hMd5gdDQ8DIRsNXzbjLRIqtuiDzCCfIb8eNFTMftKfhp7EKtBAdR2yLJz09PkdSMr8ri2SxZTClb77zl59ewZE2lnjg8Z8AJINTe3q22zdzO4TkGrYLvk8l3LvLFCYts44IjfgEQuCLmYJe+4HVn/lT5/XReqSUBHbmiqbcnvteljt/Q7e2TbW7YH7K3mfCFojsQJbQTIrLQKqTSvasMsayLZrLoIa9ZZBpShj54PPYCvCiSE93fhIh8kXaNXKQK1QR9c/c+GUwd5DHbFVUzrWYpnaiJs0btmA8MKvxFx27AGX7eTlHzHGmNSxflK7mDmycGkM7FAJJfGM8Q0AeLLt9qTq+eaqWwo4QJRlJYvxAGTvGSNOfj6cY/Wwz7hRljfaF4/gwodudT34UWkDFy36xh6utVLNuI=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "18.04" + } + ] + }, + { + "component": "server", + "platform": "windows_x64", + "file": "metavms-server_update-6.0.0.38086-windows_x64-beta.zip", + "size": 109881758, + "md5": "efabdca9e0d37bb8fc00475c1333b4ac", + "signature": "ACffywPrAIX8hzdA62P+giSEoiB+Q/0nOe59pLn6ivoG3osrTzpv7YLKPiB9w5GZ0DPyExLHRkp/98wAUw7rGkYsezFjzynA0/r5hYURj26ew7dGVz0sg+NI3adSj45o+d6rhsMao39A2cU8eTjcLm9+cgjaJFtE7EmJHv5TymNNDHKpteNIsGzNgIiZrRyUA7YM/9IUMEWEkzAe3LBygHx76V/3lBZU50Wb/pxUxA0+WuImT9uwLQWDCtG/xdHgCt9+FfjpWLCN18G9vP4QGVnEEOgXqfv6nS2FID6ylal8oCbcbmz3P6iNJHMIryztQDXkZTQ9bjCrCro+5RPKJ9NgxcxeKzuL7SglgW/uiBQ6mwLRAHQRsQidw0LLfwLKWnKfk5LjIMrOBIVfCZfQHqntDSBd0B24lq3RRILlUthrULaqn0wXv7N8BkOafSNeKICCKJb7SL4OTbpmnemc7R7y/bpVGXjTkCBq2ROn2Ank3czq2X/RdkpVWFzjC6U+eKZ2pDU5kV3EvYWQMcydTumASHo461so9axTxRlhmVx86RM7aEBVFpGgs3Jmh5+pDdLwAFVgoq6QKqLGONcaIO3GtAsD28VZL8uXZOmNf0Zd8bh1IpSoSyptQnNvIKefyVZyyaXzCKGHHbLGa66g42VKY9kdBVUM9N03mQT9Zyo=", + "variants": [ + { + "minimumVersion": "10.0" + } + ] + } + ] } \ No newline at end of file diff --git a/Samples/Packages/NxWitness.json b/Samples/Packages/NxWitness.json index 3009a25e..874161be 100644 --- a/Samples/Packages/NxWitness.json +++ b/Samples/Packages/NxWitness.json @@ -1,155 +1,155 @@ -{ - "version": "5.0.0.35270", - "cloudHost": "nxvms.com", - "releaseNotesUrl": "http://www.networkoptix.com/all-nx-witness-release-notes", - "description": "Support for Ubuntu 16.04 will be discontinued in the next release version and Systems on Ubuntu 16.04 will be blocked from updating to the next release version.", - "eula": "\n\n\n \n Nx Witness EULA\n\n\n\t

NETWORK OPTIX, INC.

\n

END USER LICENSE AGREEMENT

\n

IMPORTANT – PLEASE READ CAREFULLY: This End User License Agreement (the “Agreement”) is a legally enforceable contract between You (either an individual or an entity) and Network Optix, Inc. (the “Company”) regarding the use of the Company’s software programs accompanying or provided under this Agreement together with the SDK made available by the Company, associated media, corresponding Documentation, technical configurations, and technical data (collectively, the “Software”). “Documentation” means the user’s guide and other documentation (including print and online), if any, provided to you with the Software.

\n

BY CLICKING “I ACCEPT” BUTTON BELOW OR BY DOWNLOADING, INSTALLING OR OTHERWISE USING THE SOFTWARE, YOU CONSENT TO BE LEGALLY BOUND BY ALL THE TERMS AND CONDITIONS OF THE AGREEMENT. IF YOU ARE ENTERING INTO THE AGREEMENT ON BEHALF OF A COMPANY OR OTHER LEGAL ENTITY, YOU REPRESENT THAT YOU HAVE THE AUTHORITY TO BIND SUCH ENTITY AND ITS AFFILIATES TO THE AGREEMENT, IN WHICH CASE THE TERMS \"YOU\" OR \"YOUR\" SHALL REFER TO SUCH ENTITY AND ITS AFFILIATES. IF YOU DO NOT HAVE SUCH AUTHORITY, OR IF YOU DO NOT AGREE WITH ANY OF THE TERMS OF THE AGREEMENT, YOU MUST NOT ACCEPT THE AGREEMENT AND MAY NOT USE THE SOFTWARE.

\n

In addition, certain third party programs or code (“Third Party Software”) are being provided to you under the terms and conditions of a separate end user license accompanying such Third Party Software (“Third Party Software Licenses”), which will be provided to you by the Company as part of the Documentation or may be made available for viewing by the Company at the website or webpage at https://nxvms.com/content/libraries (the “Third Party Site”). If required by any Third Party Software License, the Company will make such Third Party Software, and the Company’s modifications to that Third Party Software, available to you on the Third Party Site. The Company may be required or wish to provide you with certain notices or information about Third Party Software or Third Party Software Licenses and you acknowledge that such notices and information may be provided to you by the posting of such notices and information on the Third Party Site.. BY INSTALLING, COPYING OR OTHERWISE USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS AGREEMENT AS WELL AS THE TERMS OF THIRD PARTY SOFTWARE LICENSES. IF YOU DO NOT AGREE TO ALL SUCH TERMS, DO NOT INSTALL OR USE THE SOFTWARE.

\n

1. License Grant. Subject to the terms and conditions of this Agreement, the Company grants you a non-exclusive, non-transferable, non-sublicensable, limited license to install and use the Software, in object code version only, for your own use subject to the restrictions contained in this Agreement.

\n

2. Backup Copies. You may make one backup copy of the Software, provided your backup copy is not installed or used on any computer and further provided that all such copies shall bear the original and unmodified copyright, patent and other intellectual property markings that appear on or in the Software. You may not transfer the rights to a backup copy unless you transfer all rights in the Software as provided in this Agreement.

\n

3. Ownership. You acknowledge that the Software and any copies that you are authorized by the Company to make are the intellectual property of, and are owned by, the Company and its licensors. The structure, sequence and organization of the Software are the valuable trade secrets and confidential information of the Company and its licensors. The Software is protected by copyright, including without limitation by United States copyright law, international treaty provisions and applicable laws in the country in which it is used. You acknowledge that the Company, or its licensors, retain the ownership of all patents, copyrights, trade secrets, trademarks and other intellectual property rights pertaining to the Software. You will take no actions which adversely affect the Company’s intellectual property rights in the Software. This Agreement is a license and not an agreement for sale. No title to, or ownership of, the Software, or any intellectual property rights subsisting therein, is transferred to you.

\n

4. Restrictions. The Software is licensed solely for your own use. The Company and its licensors reserve all rights not expressly granted to you in this Agreement. Without limiting the generality of the foregoing, you shall not nor shall you permit any other party to: (i) make copies of the Software except as expressly set forth in this Agreement, or (ii) disassemble, decompile or translate any part of the Software, or otherwise attempt to reconstruct or discover the source code of the Software, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding this limitation, or (iii) modify or create derivative works based upon the Software, or (iv) externally distribute, sublicense, resell, encumber or otherwise transfer the Software, or (v) rent, lease, lend, or use the Software, or any part thereof, for timesharing or bureau use, or (vi) allow a third party to copy, access, or use the Software (except as expressly provided in this Agreement), or (vii) alter or remove any copyright, trademark or other proprietary notice which may appear on the Software, (viii) take any action that would cause the Software to be placed in the public domain or become subject to open source license agreement, or (ix) use the Software in any manner that violates any statute, law, rule, regulation, directive, guideline, bylaw whether presently in force or may be implemented by federal, state or local authorities.

\n

5. Paid Features. The Software is made available to you at no charge or fees. However, the Company may offer certain premium features to you upon payment of a license fees determined by the Company in its sole discretion (“Paid Features”). In order to use Paid Features, you must agree to pay the license fees plus any applicable taxes through one of the payment methods accepted by the Company. Upon receipt of the license fees, the Company will either provide you a license key to activate the Paid Features or allow you to download the Software with the Paid Features. Activation of Paid Features may also be regulated through the use of transferrable license keys, and such license keys may only be transferred within your organization subject to the limitations listed in your purchase order or other documentation describing the Software and/or license rights obtained in the Software. You expressly acknowledge and agree that this Agreement will govern your use of the Paid Features.

\n

6. Updates. Except as otherwise expressly provided herein, this Agreement will govern any updates and upgrades to the Software that may be provided to you in accordance with the Company’s then-current maintenance and support policies, unless such updates and upgrades are provided under a separate license agreement. You may use such updates or upgrades only in conjunction with your then-existing the Software licensed under this Agreement. The Software and all updates and upgrades are licensed as a single product and such updates and upgrades may not be separated from the Software to exceed the scope of your original license. Any other software that may be provided with the Software that is associated with a separate end-user license agreement is licensed to you under the terms of that separate license agreement.

\n

7. No Support. Except as expressly required by applicable law, nothing in this Agreement entitles you to any support, maintenance or new versions of the Software. You may contact the Company to determine the availability of support, maintenance, and new versions, and the fees terms and conditions that would apply.

\n

8. Disclaimer of Warranties. THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OR REPRESENTATION OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. NEITHER THE COMPANY NOR ITS LICENSORS REPRESENT OR WARRANT THAT THE SOFTWARE WILL SATISFY YOUR REQUIREMENTS OR THAT IT IS WITHOUT DEFECT OR ERROR OR THAT THE OPERATION THEREOF WILL BE UNINTERRUPTED OR ERROR FREE. The Company does not warrant that functions contained in the Software will meet your requirements or that the operation of the Software will be error-free or uninterrupted. The Company is not responsible for problems caused by changes in, or modifications to, the operating characteristics of any computer hardware or operating system for which the Software or any upgrade or update is procured, nor is the Company responsible for problems which occur as a result of the use of the Software in conjunction with software of third parties or with hardware, which does not meet the Company’s specific requirements. Some jurisdictions do not allow certain disclaimers or limitations of warranties, so some of these may not apply to you.

\n

9. Limitation Of Liability. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW EVEN IF A REMEDY FAILS ITS ESSENTIAL PURPOSE, IN NO EVENT SHALL THE COMPANY OR ITS LICENSORS BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE OR THE PROVISION OF OR FAILURE TO PROVIDE SUPPORT SERVICES, EVEN IF THE COMPANY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN ANY CASE, THE COMPANY’S ENTIRE LIABILITY TO YOU OR ANY OTHER PARTY FOR LOSS OR DAMAGE RESULTING FROM ANY CLAIMS, DEMANDS OR ACTIONS ARISING OUT OF THIS AGREEMENT SHALL NOT EXCEED THE FEE ACTUALLY PAID BY YOU TO THE COMPANY FOR THE SOFTWARE, IF ANY, NOTWITHSTANDING ANY FAILURE OF ESSENTIAL PURPOSE OF ANY LIMITED REMEDY. Because some states and jurisdictions do not allow the exclusion or limitation of liability, the above limitation may not apply to you. In such states and jurisdictions, the Company’s liability shall be limited to the greatest extent permitted by law.

\n

10. Sole Remedy and Allocation of Risk. YOUR SOLE AND EXCLUSIVE REMEDY AND THE COMPANY’S SOLE AND EXCLUSIVE LIABILITY ARE SET FORTH IN THIS AGREEMENT. This Agreement defines a mutually agreed-upon allocation of risk.

\n

11. Third Party Software Licenses. Third Party Software included with the Software is licensed under the terms of Third Party Software Licenses. You understand and agree that, although provided to you by the Company with the Software, your use of Third Party Software shall be and is governed by such Third Party Software Licenses. You understand and acknowledge that Third Party Software is being provided “AS IS” WITHOUT ANY WARRANTY, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. If there is a conflict between this Agreement and any Third Party Software License with respect to Third Party Software, the provisions of such Third Party Software License shall prevail.

\n

12. Export Restrictions. You agree to comply with all export and re-export restrictions and regulations (“Export Restrictions”) imposed by the United States or any other country in which you conduct business. Without limiting the generality of the foregoing, and regardless of any disclosure made by you to the Company regarding an ultimate destination of the Software, you represent and warrant that you understand the U.S. law currently prohibits the export or re-export, directly or indirectly (including via remote access) of U.S. origin products and technology to certain proscribed countries (including Cuba, Iran, Sudan, North Korea and Syria and/or any other country that may become subject to an embargo by the United States), entities, organizations and individuals, without prior authorization from the U.S. Government. You will not commit any act or omission that will result in a breach of any such Export Restrictions. Your breach of this clause shall constitute cause for immediate termination of this Agreement.

\n

13. High Risk Activities. The Software is not designed, manufactured or intended for use or resale as on-line control equipment in hazardous environments requiring fail-safe performance, such as in the operation of nuclear facilities, aircraft navigation or communication systems, air traffic control, direct life support machines, or weapons systems, in which the failure of the Software could lead directly to death, personal injury, or severe physical or environmental damage (“High Risk Activities”). The Company specifically disclaims any express or implied warranty of fitness for High Risk Activities.

\n

14. Termination. The licenses granted herein shall automatically terminate without notice if you fail to comply with any material provision of this Agreement. In such event, you must immediately uninstall and return the Software, including all Documentation and original media, to the Company. This shall not limit or affect any remedy available to the Company for your breach of this Agreement.

\n

15. Miscellaneous.

\n

(a) If you are a unit or agency of the United States Government, the following applies: The Software is provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the Government is subject to restrictions as set forth in Subparagraphs (a) through (d) of the Commercial Computer-Restricted Rights clause at FAR 52.227-19 when applicable, or in Subparagraph 252.227-7013 (c)(1)(ii) of the Rights in Technical Data and Computer Software at DFARS, and in similar clauses in the NASA FAR Supplement. Contractor/manufacturer is Network Optix, Inc.

\n

(b) This Agreement shall be governed by the laws of the State of California, exclusive of its choice of law rules. Any dispute between you and the Company regarding this Agreement will be subject to the exclusive jurisdiction of the state and federal courts sitting in the County of Los Angeles in the State of California. In any such action, the prevailing party will be entitled to costs and attorneys’ fees.

\n

(c) You may not assign, sublicense, or transfer this Agreement, the Software, or any rights or obligations hereunder without prior written consent of the Company. Any such attempted assignment, sublicense, or transfer will be null and void. The Company may terminate this Agreement in the event of any such attempted assignment, sublicense, or transfer.

\n

(d) This Agreement is in the English language only, and will only be provided in the English language, which language will be controlling in all respects, and all versions hereof in any other language will not be binding on the parties hereto. All communications and notices to be made or given pursuant to this Agreement must be in the English language.

\n

(e) The Agreement constitutes the final and complete understanding between you and the Company with respect to the subject matter of this Agreement. Any modifications or waivers of this Agreement must be in writing and signed by both parties hereto.

\n

(f) If any provision in this Agreement is invalid or unenforceable or contrary to applicable law, such provision shall be construed, limited, or altered, as necessary, to eliminate the invalidity or unenforceability or the conflict with applicable law, and all other provisions of this Agreement shall remain in effect.

\n

(g) Failure or delay on the part of the Company to exercise any right, power, privilege, or remedy will not constitute a waiver of, or bar the later exercise of, that or any other right, power, privilege, or remedy of the Company.

\n

(h) No rule of strict construction shall apply against or in favor of either party in the construction and interpretation of this Agreement.

\n

(i) This document was originally written in the English language, and the English text of this document shall prevail over any translation hereof.

\n

 

\n\n\n", - "eulaVersion": 1, - "packages": [ - { - "component": "server", - "platform": "linux_x64", - "file": "nxwitness-server_update-5.0.0.35270-linux_x64.zip", - "size": 89567537, - "md5": "2c3ef4243b96c804495641659957bf74", - "signature": "n/mHdUgx8uEwSnLsUXPmUKLriXNSeKNODq6d8ANMzNkvrE0UDXqpzbhPXvoOyt4pgdT0EPTphYDnY7KVQAs4aZxhu47aq1kAGAvoEtRpPs1BbhwHOdxhUZIEZroZJhJiI4bbXuzCZsDGstfX+Ti0tN9aGJehIxLLU5qHVEfUlVuMlrYqMyElWqBgj68Z6xcM4QS8K7q2MI5Uo5qkNzcnV+Il5LdgIJDLs9WIDlj5eLO7ul6Cn4mBGdH6ffLJtD5uXuelfzGvSgh6iO/GKV/Nzs+5lp4cscqb5/2mn2e10aYDlGmJ5k6eI5dcXdxHksxQv/42cnYWRq2GA+hYOk+hMIZn6svSrsOzVKktvA0bIOC7IYc4pWEFs5X1FGEy95fUyanwQLv6LPUE5xq+/IILSPX/dQFpKOVOvkYwtLmqPzfUkrTX0PA6bG02eOItQlJ+xXulGcTlEMc4s2TPsSeabyNLuLX7hex3Ew3+BuNZ0GTB1M0lYV2ho+BPpkJYt47IpfLViE1IFxPmjyZu2w5X4hDYZbYukfPrETsaGincWljBCgTeJ4ZMeGHujBqjQkrfauWLw4akvvduy50AVKeX+4dbf3d7urkA0xJFyWdOmXVFJkDsdTThYQK0pOcpxEyH30Yls3eLkPtC3cDlWTLMEeZXvn6Mf1ScojqYRoFhKxc=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "16.04" - } - ] - }, - { - "component": "server", - "platform": "windows_x64", - "file": "nxwitness-server_update-5.0.0.35270-windows_x64.zip", - "size": 112246778, - "md5": "a915fecbe66e2a10eb54b98e50e98f86", - "signature": "wbJrtCZnHWv6v6TBbQNiKc7r6vJ3A2hRLih7oyPvfryATVPgliOJ2/y9h0Gf4l4+8HDxdwCh0pOwN8LDi3CauEs2h4cwEnz1ysa+Z9ZiguQ/6+AMYOW4NgAJd1J6kv5x1NegS0oZ9X2X+a6R9DTWJFuY0aVAtuf/9d/srR8zJnMAYH1jJUk1NP6DqQ3/DCDsaK1lazqR+OjWkCDs2Y3dCEroW5Q2IM1H8gR4nXQO9XTMbo9IpvV1ENp8ogGlJa7xTCOh9drMfNXrex92NZdnrSlNsx/9b9M73tMuH3+0ZfNytL0dPOzCSkYQthVZN5wmtgTzjW2UOtBiCGmbeGPO5/ZlBJbnmV3iwDpv1XiUvWLhJfdhsKk6VM9gXgcC8UExHpxeT88uhxG9F4QSKOKbgvhrh2imqRQiD64hvThBA0I98UjiddtCvDDUMm2v3ScAPUjl8SSiCzKYsfBLfnfgFYOa7VscK/+8Y3v8gOSHRWTCVb+lWm+nuAnBGk52IQT7kHV8U5L3b6L+r+nEVVHPKyNrpkhmCpM4+ld9e887oUIecCZrbOrpsYEdqCOjZyNyQh/A98cYDZdAOiztxJOnGr8tIwDFLryzRHz9tQ9AIAoadOT+NvAOzEDM+u2G6nSu7OsdQXzPU6vyFM3ItIqclfaiu7g8+9/58pqDh/NX8ds=", - "variants": [] - }, - { - "component": "server", - "platform": "edge1", - "file": "nxwitness-server_update-5.0.0.35270-edge1.zip", - "size": 85894365, - "md5": "b222e3008273335f17a421a58e8392ae", - "signature": "MlAJf3klqKYCVYR9Jf74bK5/ekicsqP56O5rqxBFzZ/g99ZZpFkI/2jTDSM7VcrY1fpjVTsyTSf0Tci7Mk+quB1bzYlU7UKLslmmsKwPxv1Ho3bYZBL2ufcyv/aljZNxg1OS9EihMmit3O/7w8KEnGRLWsToIB6VN737W/6wuAqfzBv6gBEjHyEDk3Vwi90BYXFgAP7I5NrN6ugcy94yzIFS7RuWsY6NWwW4O7NS8o/vr6N4szT3lYIC+rrMa0xoyB7yAxATTp/NIhWUZXV9cmAS0xulPt9bgXIcz8w7LtyHAOmdQbOnWcN11QIpwRrtTIykVmY5Cz2dfREa94IN1Rgu+/XrChhyvIIGfoY+qrG5co0sdAJQZEnvrjr3iVmyp/U4bx6ZL7VRLheHT/GGUvny7jW5jryS/cDQST+isXBSc8QOEK0dtgVhVwlLFT5cwvQ1E1Fa+sNYw7A8VQzRdjhvfZVUSIyEbrWOpy8mA2L2axrhXoKUOhvQ5d5/+vyjQumHUCDFuVXPabX6cr0hLzPV+Riz7u+R/yZncVV9V7OeIFtOw2rtgQ5NJb/mer4Z9230gzu/uFu6Gm1WGi31G4rxMUq2QklpEQRWrMGeOfzrDszQuVJc+XOWrBjLYDideqScDy728NGBWGomtsLePyF7d9ABMcmwa4U5yoVIfNY=", - "variants": [] - }, - { - "component": "server", - "platform": "linux_arm64", - "file": "nxwitness-server_update-5.0.0.35270-linux_arm64.zip", - "size": 146000167, - "md5": "af4dcd0463070803191c759bb9c2cc24", - "signature": "jmiKRBpvkEdPPOzQhp1efSY4l1vjx8hZEJrO9Ariyp1c8kgdERAnDmNQZzRcdekcQKJtNwi757UOZI/UcxWJp+TNyDQwfERs1Fk1JB/cSX0flYzcBMIdVe4LAFBoBDE/3Asu+hYYOZFP1QYqB2Am7uRRMMUtvhe59U384ykdiM+ueJ35MKV7FnzF4uj1/b80UBJTb3CmFocxvgwK1+aZjoFpSWMJj1DqJ7WRBKL+wwdfN11DprRw6eMUqc4mL7ddRnIjCWAJr8vXGOZnMT/+rXxizhcQTJqrxL/OUGa3kUqNzyhyDyOZfO1vxyg86hUZ+ztRmu0ekuizbAXjQEq6zmQN5teXyBf6t4UJrq7kjaZWSaS4czelYQyLe6ikFjUMM/ROIagwpjACfhR/NLo313hEzIrd/r3cnA/uWdKfjCs22kP7U+c99fno/LW4Z1LgX5/O34iHGveHs30Gigv33Iv0u8+CEqTZsWSUTcbflVwpk4JCUf0XzpC48n+rqP4wAbcauPQTWVXC+O+EUjS4O+zjJL5Vl1f4IGTwegwBYi4OceOJiZYFgIxV6JTgDVvtLRvn3+u7ec2KvOS0v4U6cBhJPOs66Qf3O0Wvm9/NaYpNr183F9r1n88rPekFBgGrAycSgJszvnibtCq//3S7nTKBSAtf9zwYMsYLaGeMxkA=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "16.04" - } - ] - }, - { - "component": "server", - "platform": "linux_arm32", - "file": "nxwitness-server_update-5.0.0.35270-linux_arm32.zip", - "size": 169700173, - "md5": "e591ad3c572bb08c077e1189817ef9fb", - "signature": "M6u0J+rxn0+D/J7sA6sT8cPMvhxynRMLY+IX1CZKne1gQSY1VwsZAYjev25L0HGULM7Pyvuzzwykgbn4rsSieps/mNays0RAjRUVOTKMs1ZndZ7iQy6oitDdpxF5iU8Zy1mYyf3idFJmDRUlu7YlmzLHkVmIoAFCReEISPceNA/r1IWQRzC5povzUPk5JcQA0Kv3KL4Hbo6dgNCk3gZR/KaGCC6mPygQTkVjeXOQn3Ci8tH6DS8lzlwOR5heLGjm7+akQ8ETCksbC2zrHpnSQCQVJGfjwkCrG/LhBcjDOST6ucga8Q1HGqHseXYF7xi6s7/3OwpKKydVvQ6mtkxXHL8PVlqjRCdcQCHyJpB38S2V3n4dXagfzROS4GNvgFS3mq6Br87HBZDoQSta0yH9lwscwnIh6V3SRSpY1e8/4kzERXTkYKqAeoy7naMKqGSVzHEUb9wRolvl6oZhiLkjntk8txyj2YkpLb8ZZjTfU99PwenIVwn8nfNvsso02UoBFqZXRWOu84qmSWf7W48f6A38KaXVvdjtEwFsBeTQe9wRc+lHH4kLNGQ3RQCyTm7eC8QcJsCjNAk4SJDCeBCH2i77mno3Mug38CmqzZQLmjjbY5dMLgepowQpjqXKm6f5UbyTkreaShVu4YMWTGNuk41o4LDYkafavSfFKxl1Hh0=", - "variants": [ - { - "name": "debian", - "minimumVersion": "9" - }, - { - "name": "ubuntu", - "minimumVersion": "16.04" - }, - { - "name": "raspbian", - "minimumVersion": "9" - } - ] - }, - { - "component": "client", - "platform": "linux_x64", - "file": "nxwitness-client_update-5.0.0.35270-linux_x64.zip", - "size": 271453787, - "md5": "d1276fd672b9c32a1b0ed7b7529763ac", - "signature": "eGpTGO4Gg+WCgqOAcPBbuSdCM1Hfv46AlYWI1qZIa2KHax9B47VEdo3wSHdhcyMjjXxn3mavU5zIVNOcx7b5ovr0qQKMBC3OBV/ZWnp2iInB8LKd8VjvMk7sBiegaBTgdDqxHIeXNTlE2bvIVdhPalWHZeUPILzfWdtdOhE4sO4Csej8ZGrT8TkcleVG12/gZMjXa3Wof8FpXt95994+0RpxP/Ec4Z8IHxZP0nN0e3zLF7/m6z305wgrTEUfpMlYOQbb9d/7O5KPEcK6IjIOHQ+9VlczdYAzuXpz2WqR6UOzbtG+cKLTuiAEjLjvRpGRowYmVGpL3R2vCp47EsFrWveFpYUaBgsO+/Wz2X+E4veN3GY3uM/MZmNncOsROYxJ1MDV2AFkMjB/6lfzwcND+/q6IHLL3APPoYLXicvH29A+NBZhudy6AqZ2M6t3Uhj8VHvYz/vU+uN8CDsvozv/eid+do9oVzFd0iZcMfXHjRCbg2YjXdtTdrfkYLkCip0Yx3PbP8GZWs7Z8G6jmr8iZZxmt03aotraXWGvjtFcKQOKM+r+d9irDNNsYiuf2AvQGCOkwl4EnQSoxyWpGpqHISOFX8VbOYPXmhbdEr+8aJmArMzcVj0QVXdaSWEZsqbJ4yfRXFfx0ipaTSptlcBTl5M6kEBEWcTRFCRlPEzNCKs=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "16.04" - } - ] - }, - { - "component": "client", - "platform": "windows_x64", - "file": "nxwitness-client_update-5.0.0.35270-windows_x64.zip", - "size": 156180611, - "md5": "2462fb805a75275244ebac1b1f349472", - "signature": "CJsqytA3QieL/yQJ7fQEQXdgPI+CnbkHTjVArcm+kXOCtzLsehx/j9z4vsjhOilXQTY2mYPe+44OkziNq6GwPoj3Vd3CxmWep8pdySFPu5CJdM3aDPByvTbz2On+Ps+/HUXReroMmB0Jx/KHaU0bNBhXz2GjauYp+XmAbY2eCQrPc6W0l7LO7/nD0Rs7oM8YDtqZ5m/2p4UgaXzeencETyNUFgYc3Vixj+RRI8lHj29/oLS/f8l6/dnXKi3M5G9iuqBRvHvZc7CY+iTImPxNO6QbPbktnZEKNCtgh9kTTjp5ekGokk75R90kheoOrl+Z8U6T9G4mlmpK6SbSb3N8TCNNhesXucD0m2O0akWIER/m6B/hFdZ6AbjvwJZNIL1YMMh9wWluwsfCaVfgyihPVQHS25EkeZ8iGMjvfxWhZA4ADhk89Nbx0K32+CSBZjT70Jk8v6A3U5wTMFQHVFYFeH/nfCN1rHcJU6YYZr70iKGOG7qcN6QMvWn8/oadHGO7BanK9HXfy2o8KSNm0joT2XVwDBZwyfNdrTDGEDp3JVz5vW+dXVtywbBvNVm2l1OmZeKGHx7n9BhPhRga3VlntZESNUtrQQE91VtNb7bGn4YB7sHYXCg1M4OwmZ7cpuBuwbVL4adJ4JAYy3AhtqPCLi0NZhcguVJBz1+obbcHEAs=", - "variants": [] - }, - { - "component": "client", - "platform": "macos_x64", - "file": "nxwitness-client_update-5.0.0.35270-macos_x64.zip", - "size": 160431892, - "md5": "27ccd9decc6e12bb278d2445eef7a96a", - "signature": "aaj7K0xoWA5QsebnWtOQZnZw47Vh5GGXJlhazaCpkl1gnyaQ/EybuQg2m+v19KqoM+MtqFrbkh13fQVFvKNZ7ynPVz+EDQ6nTvblkKt01IFo9jzO4Gfvnxycq7prMl6kf8abg2Tf3Krhnrkm1Pcv+qYVkte1fgbF7umrD2sfSXbs7KxjKJV2Zeh2NJfKLrmshLU0YVjjEHmjwpasVzX6hDcQ7xavWxGIOf6uvZKgbENH2x8AiOlhbumkl1NLhD/PmWIOndE8buD8Erhy5cNlbO0JCxserF/ly6+NqwhneRXRWfMCHD/acdtrkK6GYWr+JfTmSdkmqwqGLRbOPDKeSneLrbc+RXse30XzlyaD/hzM2X3mvtaPtlfuaj5lnKg0AsYaxNL5QZ68lQDDA6mW/rJnMjl2nz7K/gKdmTlgd2sAjjfGQILaH+TiYKhTGv4gjurdfmrhUxY89w873XbLUZYsb1D/aoINXczpp8gmu7cEFw8BVgEjkNbvoME98OEGxllH8X1vtpB5/7jqVvhKtiHUJ8Y/TgfvMWwtD+KEVV6C2cH3+RsJpDIIDlux8/GNXHzmUUqdRS832nL4jgbSbePXmvJI4yGHZbbCZ1UM6TePtsDGvpAb83I1Rj+KjsBkPZppgwVdMY5jDXkFyDso/MF5QAZHzD5zTySZtu8fzIs=", - "variants": [] - }, - { - "component": "client", - "platform": "macos", - "file": "nxwitness-client_update-5.0.0.35270-macos.zip", - "size": 160431888, - "md5": "a0a7ec56f0efdec004491379354130b6", - "signature": "brtTWDXlH2OLY5D51vus8XEvyWuJqaL+rw4CVdiD18ms3w/OXTO+QHtHmRwbiNFm9zLzgiEM1snhQkIyLFJEC24XnYjwt2BxbWtdK1BOUzwJdNiglIkHtQQpYeVM5NK1teu78AMtKnuF5cKJNZ9J4fYuQqPhOYqLnXNaa9Sy2gq/B/P9kp21r58ov4hAYxSjrOTALIoyKLGQlUE452NXj1ng4t4ueujKrwT19gwq17hx4G5N5DEwrMmrbh2VLANl7I0FyyjwWo6I65Cn+AzAV/wXadoflguCEUMeS12mDzmZrd31l7yVd4SSvgbfdNNOaPWW5RgmjsJ9B+e5NQqB6JH99T8Y0CCvF1x+K9mtz4xtyQ7xBvZZudZSVQBACvMfzKgd/YgLPdEao72MwOpE2KZj5Rke9l+hDImcDPD/TEold26QMZHO3qgspKwmgTLVUrijSThuCCFvc57o1N8w8rruPC0pFyqJbexOTrnrGBMcEFFQ9XaIA5uc3/BN99TwOt2GddPpK5aXOxdTymMjB3kCxrw8sVO9zKk4P7sFYTIT1STuB9/2mCKy/s3DdmPENbiuzypE8LXmChIaecJK4R+XJuwx2fPgICg6Yp6M9QRiqXeFf/XevSXugZnbcyW0lpWwbzBVXPP/BftNp8+BTojsQb1TppZxxByb6k7j9vE=", - "variants": [] - }, - { - "component": "client", - "platform": "macos_arm64", - "file": "nxwitness-client_update-5.0.0.35270-macos_arm64.zip", - "size": 146991991, - "md5": "0f6741c39c00d5c7af8d9b13a25f8fcf", - "signature": "J66YBALqFUTKGYcHMe3FoDzvgcePvO1+uWY2963e1HMJMgSZ8gow9uqN2aV1wxN+Cja6vF0RHJ6JPmyIT2nH/TgGbDo4wG5N3xN2Dpt6CdVY9cF0ClMlnZYbZ8+o/NtR9bX78cEt0EK4BEHfKP1djYnjaBjBlH2s1ihciCwhxNt1vst9MpPiZK2k2+SFp2W1DnnvKXuwntECOaPfNBgBpzdap3EmZnRCQ8oOTGbgbz8pfn9YM4Rg7ZNVf8XWzSldMpuSHmS17dmnNobGF87T7WDV5VgShHrMmMoRsx7yvSS3zN1OD7fL0oEIREuBa191QXvc22T1icN6AXr03MESyy0HD8N2L6SVpoStq+dfRtRSO4NJX+26bcsxoMI4Ddhl/XAz88tGfW2o1vQiS3vx6UaXt/Uaw8OZoYsJREqtYsHSf2rUSMhbR76eD3P3apSp01rAzCcI4ZCwuJAJDlzWNr1oRNGzOSQFGhaRebqJNZINN5xswSbbPxyMSOD7x0MkFHP4Du77I6MvxROwzqCv+jB18En/KblazOGOhS4WP8NAbtV5s21nxerU5CbuMQl8CuuMi8DgwmuveYOsN0XXdk4F/B5SArJszEewqXj1BVfKYmTgJlbSJoF5XS8Hq77S+YispsHR/IaBHxe5M14X4pYK4yCmJA/IN6lBm18QOKk=", - "variants": [] - }, - { - "component": "client", - "platform": "linux_arm64", - "file": "nxwitness-client_update-5.0.0.35270-linux_arm64.zip", - "size": 276050812, - "md5": "83bac919b14dbf30d471b0746f666e30", - "signature": "HXGNqsprAgu+nzs2EJ/NB6ymjsTlphXXyw/ZWnEXQgghtoRqwkPuK2aOfV++gfN+SyzDFfDsC78ELfVD8ExoPlS7FHRpMCBuN3cKcVopFVRoaVuzNwObzsrkxE4A36ZwDqIE9c+lfDNsI8VMuBsEDm3kVscP4du2aqtGeFnc277JMKJIHzPfdWNO2Z5dptGiou/+86EGJAIUrQwon4vWkrZxSC4dg0nOXtQ9rhHhLQg1ArJ4+n9IyNdSbyzSBfGBX6HB1n7GNCWv5JgHRzXJyiS6CGr2HyE6SL8x1FmMV1s2UqNzjX5Co4sp61RYREatVtKvBIDwEIb+G1IsWKriaX7tW/DCKE9QeA4TtzMDPKmhg8LvxBiNmTsY7ZlUDwbZ2nD9lo/0DJmvMEUXxAMAYXSQPX74qu24YSMQWOgBODeypNsqQrRLlpCTnC4t1VS+cYdD8dlwm6Q5485TraJ3lZzTlISUTjuI1tLiJGZmnQklFTcW+hOHjoyo/AdKgG/MmK34jEtKoRbY0d6jRQiAryEHa70M7dYu038Ulz7Hls/nsDAo4RdCAx85LJZeunH4hifnqt/Z5dcFBuP0FNuKLghy+8cBG585HqhKj0qs7PSNWUa1azEPLJq1AoaaOaWTHBlJm81HocY42XHYx13rLfNK46eqIqhAvcsreClg18U=", - "variants": [ - { - "name": "ubuntu", - "minimumVersion": "16.04" - } - ] - }, - { - "component": "server", - "platform": "nx1", - "file": "", - "size": 0, - "md5": "", - "variants": [ - { - "name": "", - "minimumVersion": "3000" - } - ] - } - ] +{ + "version": "5.0.0.35270", + "cloudHost": "nxvms.com", + "releaseNotesUrl": "http://www.networkoptix.com/all-nx-witness-release-notes", + "description": "Support for Ubuntu 16.04 will be discontinued in the next release version and Systems on Ubuntu 16.04 will be blocked from updating to the next release version.", + "eula": "\n\n\n \n Nx Witness EULA\n\n\n\t

NETWORK OPTIX, INC.

\n

END USER LICENSE AGREEMENT

\n

IMPORTANT – PLEASE READ CAREFULLY: This End User License Agreement (the “Agreement”) is a legally enforceable contract between You (either an individual or an entity) and Network Optix, Inc. (the “Company”) regarding the use of the Company’s software programs accompanying or provided under this Agreement together with the SDK made available by the Company, associated media, corresponding Documentation, technical configurations, and technical data (collectively, the “Software”). “Documentation” means the user’s guide and other documentation (including print and online), if any, provided to you with the Software.

\n

BY CLICKING “I ACCEPT” BUTTON BELOW OR BY DOWNLOADING, INSTALLING OR OTHERWISE USING THE SOFTWARE, YOU CONSENT TO BE LEGALLY BOUND BY ALL THE TERMS AND CONDITIONS OF THE AGREEMENT. IF YOU ARE ENTERING INTO THE AGREEMENT ON BEHALF OF A COMPANY OR OTHER LEGAL ENTITY, YOU REPRESENT THAT YOU HAVE THE AUTHORITY TO BIND SUCH ENTITY AND ITS AFFILIATES TO THE AGREEMENT, IN WHICH CASE THE TERMS \"YOU\" OR \"YOUR\" SHALL REFER TO SUCH ENTITY AND ITS AFFILIATES. IF YOU DO NOT HAVE SUCH AUTHORITY, OR IF YOU DO NOT AGREE WITH ANY OF THE TERMS OF THE AGREEMENT, YOU MUST NOT ACCEPT THE AGREEMENT AND MAY NOT USE THE SOFTWARE.

\n

In addition, certain third party programs or code (“Third Party Software”) are being provided to you under the terms and conditions of a separate end user license accompanying such Third Party Software (“Third Party Software Licenses”), which will be provided to you by the Company as part of the Documentation or may be made available for viewing by the Company at the website or webpage at https://nxvms.com/content/libraries (the “Third Party Site”). If required by any Third Party Software License, the Company will make such Third Party Software, and the Company’s modifications to that Third Party Software, available to you on the Third Party Site. The Company may be required or wish to provide you with certain notices or information about Third Party Software or Third Party Software Licenses and you acknowledge that such notices and information may be provided to you by the posting of such notices and information on the Third Party Site.. BY INSTALLING, COPYING OR OTHERWISE USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS AGREEMENT AS WELL AS THE TERMS OF THIRD PARTY SOFTWARE LICENSES. IF YOU DO NOT AGREE TO ALL SUCH TERMS, DO NOT INSTALL OR USE THE SOFTWARE.

\n

1. License Grant. Subject to the terms and conditions of this Agreement, the Company grants you a non-exclusive, non-transferable, non-sublicensable, limited license to install and use the Software, in object code version only, for your own use subject to the restrictions contained in this Agreement.

\n

2. Backup Copies. You may make one backup copy of the Software, provided your backup copy is not installed or used on any computer and further provided that all such copies shall bear the original and unmodified copyright, patent and other intellectual property markings that appear on or in the Software. You may not transfer the rights to a backup copy unless you transfer all rights in the Software as provided in this Agreement.

\n

3. Ownership. You acknowledge that the Software and any copies that you are authorized by the Company to make are the intellectual property of, and are owned by, the Company and its licensors. The structure, sequence and organization of the Software are the valuable trade secrets and confidential information of the Company and its licensors. The Software is protected by copyright, including without limitation by United States copyright law, international treaty provisions and applicable laws in the country in which it is used. You acknowledge that the Company, or its licensors, retain the ownership of all patents, copyrights, trade secrets, trademarks and other intellectual property rights pertaining to the Software. You will take no actions which adversely affect the Company’s intellectual property rights in the Software. This Agreement is a license and not an agreement for sale. No title to, or ownership of, the Software, or any intellectual property rights subsisting therein, is transferred to you.

\n

4. Restrictions. The Software is licensed solely for your own use. The Company and its licensors reserve all rights not expressly granted to you in this Agreement. Without limiting the generality of the foregoing, you shall not nor shall you permit any other party to: (i) make copies of the Software except as expressly set forth in this Agreement, or (ii) disassemble, decompile or translate any part of the Software, or otherwise attempt to reconstruct or discover the source code of the Software, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding this limitation, or (iii) modify or create derivative works based upon the Software, or (iv) externally distribute, sublicense, resell, encumber or otherwise transfer the Software, or (v) rent, lease, lend, or use the Software, or any part thereof, for timesharing or bureau use, or (vi) allow a third party to copy, access, or use the Software (except as expressly provided in this Agreement), or (vii) alter or remove any copyright, trademark or other proprietary notice which may appear on the Software, (viii) take any action that would cause the Software to be placed in the public domain or become subject to open source license agreement, or (ix) use the Software in any manner that violates any statute, law, rule, regulation, directive, guideline, bylaw whether presently in force or may be implemented by federal, state or local authorities.

\n

5. Paid Features. The Software is made available to you at no charge or fees. However, the Company may offer certain premium features to you upon payment of a license fees determined by the Company in its sole discretion (“Paid Features”). In order to use Paid Features, you must agree to pay the license fees plus any applicable taxes through one of the payment methods accepted by the Company. Upon receipt of the license fees, the Company will either provide you a license key to activate the Paid Features or allow you to download the Software with the Paid Features. Activation of Paid Features may also be regulated through the use of transferrable license keys, and such license keys may only be transferred within your organization subject to the limitations listed in your purchase order or other documentation describing the Software and/or license rights obtained in the Software. You expressly acknowledge and agree that this Agreement will govern your use of the Paid Features.

\n

6. Updates. Except as otherwise expressly provided herein, this Agreement will govern any updates and upgrades to the Software that may be provided to you in accordance with the Company’s then-current maintenance and support policies, unless such updates and upgrades are provided under a separate license agreement. You may use such updates or upgrades only in conjunction with your then-existing the Software licensed under this Agreement. The Software and all updates and upgrades are licensed as a single product and such updates and upgrades may not be separated from the Software to exceed the scope of your original license. Any other software that may be provided with the Software that is associated with a separate end-user license agreement is licensed to you under the terms of that separate license agreement.

\n

7. No Support. Except as expressly required by applicable law, nothing in this Agreement entitles you to any support, maintenance or new versions of the Software. You may contact the Company to determine the availability of support, maintenance, and new versions, and the fees terms and conditions that would apply.

\n

8. Disclaimer of Warranties. THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OR REPRESENTATION OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. NEITHER THE COMPANY NOR ITS LICENSORS REPRESENT OR WARRANT THAT THE SOFTWARE WILL SATISFY YOUR REQUIREMENTS OR THAT IT IS WITHOUT DEFECT OR ERROR OR THAT THE OPERATION THEREOF WILL BE UNINTERRUPTED OR ERROR FREE. The Company does not warrant that functions contained in the Software will meet your requirements or that the operation of the Software will be error-free or uninterrupted. The Company is not responsible for problems caused by changes in, or modifications to, the operating characteristics of any computer hardware or operating system for which the Software or any upgrade or update is procured, nor is the Company responsible for problems which occur as a result of the use of the Software in conjunction with software of third parties or with hardware, which does not meet the Company’s specific requirements. Some jurisdictions do not allow certain disclaimers or limitations of warranties, so some of these may not apply to you.

\n

9. Limitation Of Liability. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW EVEN IF A REMEDY FAILS ITS ESSENTIAL PURPOSE, IN NO EVENT SHALL THE COMPANY OR ITS LICENSORS BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE OR THE PROVISION OF OR FAILURE TO PROVIDE SUPPORT SERVICES, EVEN IF THE COMPANY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN ANY CASE, THE COMPANY’S ENTIRE LIABILITY TO YOU OR ANY OTHER PARTY FOR LOSS OR DAMAGE RESULTING FROM ANY CLAIMS, DEMANDS OR ACTIONS ARISING OUT OF THIS AGREEMENT SHALL NOT EXCEED THE FEE ACTUALLY PAID BY YOU TO THE COMPANY FOR THE SOFTWARE, IF ANY, NOTWITHSTANDING ANY FAILURE OF ESSENTIAL PURPOSE OF ANY LIMITED REMEDY. Because some states and jurisdictions do not allow the exclusion or limitation of liability, the above limitation may not apply to you. In such states and jurisdictions, the Company’s liability shall be limited to the greatest extent permitted by law.

\n

10. Sole Remedy and Allocation of Risk. YOUR SOLE AND EXCLUSIVE REMEDY AND THE COMPANY’S SOLE AND EXCLUSIVE LIABILITY ARE SET FORTH IN THIS AGREEMENT. This Agreement defines a mutually agreed-upon allocation of risk.

\n

11. Third Party Software Licenses. Third Party Software included with the Software is licensed under the terms of Third Party Software Licenses. You understand and agree that, although provided to you by the Company with the Software, your use of Third Party Software shall be and is governed by such Third Party Software Licenses. You understand and acknowledge that Third Party Software is being provided “AS IS” WITHOUT ANY WARRANTY, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. If there is a conflict between this Agreement and any Third Party Software License with respect to Third Party Software, the provisions of such Third Party Software License shall prevail.

\n

12. Export Restrictions. You agree to comply with all export and re-export restrictions and regulations (“Export Restrictions”) imposed by the United States or any other country in which you conduct business. Without limiting the generality of the foregoing, and regardless of any disclosure made by you to the Company regarding an ultimate destination of the Software, you represent and warrant that you understand the U.S. law currently prohibits the export or re-export, directly or indirectly (including via remote access) of U.S. origin products and technology to certain proscribed countries (including Cuba, Iran, Sudan, North Korea and Syria and/or any other country that may become subject to an embargo by the United States), entities, organizations and individuals, without prior authorization from the U.S. Government. You will not commit any act or omission that will result in a breach of any such Export Restrictions. Your breach of this clause shall constitute cause for immediate termination of this Agreement.

\n

13. High Risk Activities. The Software is not designed, manufactured or intended for use or resale as on-line control equipment in hazardous environments requiring fail-safe performance, such as in the operation of nuclear facilities, aircraft navigation or communication systems, air traffic control, direct life support machines, or weapons systems, in which the failure of the Software could lead directly to death, personal injury, or severe physical or environmental damage (“High Risk Activities”). The Company specifically disclaims any express or implied warranty of fitness for High Risk Activities.

\n

14. Termination. The licenses granted herein shall automatically terminate without notice if you fail to comply with any material provision of this Agreement. In such event, you must immediately uninstall and return the Software, including all Documentation and original media, to the Company. This shall not limit or affect any remedy available to the Company for your breach of this Agreement.

\n

15. Miscellaneous.

\n

(a) If you are a unit or agency of the United States Government, the following applies: The Software is provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the Government is subject to restrictions as set forth in Subparagraphs (a) through (d) of the Commercial Computer-Restricted Rights clause at FAR 52.227-19 when applicable, or in Subparagraph 252.227-7013 (c)(1)(ii) of the Rights in Technical Data and Computer Software at DFARS, and in similar clauses in the NASA FAR Supplement. Contractor/manufacturer is Network Optix, Inc.

\n

(b) This Agreement shall be governed by the laws of the State of California, exclusive of its choice of law rules. Any dispute between you and the Company regarding this Agreement will be subject to the exclusive jurisdiction of the state and federal courts sitting in the County of Los Angeles in the State of California. In any such action, the prevailing party will be entitled to costs and attorneys’ fees.

\n

(c) You may not assign, sublicense, or transfer this Agreement, the Software, or any rights or obligations hereunder without prior written consent of the Company. Any such attempted assignment, sublicense, or transfer will be null and void. The Company may terminate this Agreement in the event of any such attempted assignment, sublicense, or transfer.

\n

(d) This Agreement is in the English language only, and will only be provided in the English language, which language will be controlling in all respects, and all versions hereof in any other language will not be binding on the parties hereto. All communications and notices to be made or given pursuant to this Agreement must be in the English language.

\n

(e) The Agreement constitutes the final and complete understanding between you and the Company with respect to the subject matter of this Agreement. Any modifications or waivers of this Agreement must be in writing and signed by both parties hereto.

\n

(f) If any provision in this Agreement is invalid or unenforceable or contrary to applicable law, such provision shall be construed, limited, or altered, as necessary, to eliminate the invalidity or unenforceability or the conflict with applicable law, and all other provisions of this Agreement shall remain in effect.

\n

(g) Failure or delay on the part of the Company to exercise any right, power, privilege, or remedy will not constitute a waiver of, or bar the later exercise of, that or any other right, power, privilege, or remedy of the Company.

\n

(h) No rule of strict construction shall apply against or in favor of either party in the construction and interpretation of this Agreement.

\n

(i) This document was originally written in the English language, and the English text of this document shall prevail over any translation hereof.

\n

 

\n\n\n", + "eulaVersion": 1, + "packages": [ + { + "component": "server", + "platform": "linux_x64", + "file": "nxwitness-server_update-5.0.0.35270-linux_x64.zip", + "size": 89567537, + "md5": "2c3ef4243b96c804495641659957bf74", + "signature": "n/mHdUgx8uEwSnLsUXPmUKLriXNSeKNODq6d8ANMzNkvrE0UDXqpzbhPXvoOyt4pgdT0EPTphYDnY7KVQAs4aZxhu47aq1kAGAvoEtRpPs1BbhwHOdxhUZIEZroZJhJiI4bbXuzCZsDGstfX+Ti0tN9aGJehIxLLU5qHVEfUlVuMlrYqMyElWqBgj68Z6xcM4QS8K7q2MI5Uo5qkNzcnV+Il5LdgIJDLs9WIDlj5eLO7ul6Cn4mBGdH6ffLJtD5uXuelfzGvSgh6iO/GKV/Nzs+5lp4cscqb5/2mn2e10aYDlGmJ5k6eI5dcXdxHksxQv/42cnYWRq2GA+hYOk+hMIZn6svSrsOzVKktvA0bIOC7IYc4pWEFs5X1FGEy95fUyanwQLv6LPUE5xq+/IILSPX/dQFpKOVOvkYwtLmqPzfUkrTX0PA6bG02eOItQlJ+xXulGcTlEMc4s2TPsSeabyNLuLX7hex3Ew3+BuNZ0GTB1M0lYV2ho+BPpkJYt47IpfLViE1IFxPmjyZu2w5X4hDYZbYukfPrETsaGincWljBCgTeJ4ZMeGHujBqjQkrfauWLw4akvvduy50AVKeX+4dbf3d7urkA0xJFyWdOmXVFJkDsdTThYQK0pOcpxEyH30Yls3eLkPtC3cDlWTLMEeZXvn6Mf1ScojqYRoFhKxc=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "16.04" + } + ] + }, + { + "component": "server", + "platform": "windows_x64", + "file": "nxwitness-server_update-5.0.0.35270-windows_x64.zip", + "size": 112246778, + "md5": "a915fecbe66e2a10eb54b98e50e98f86", + "signature": "wbJrtCZnHWv6v6TBbQNiKc7r6vJ3A2hRLih7oyPvfryATVPgliOJ2/y9h0Gf4l4+8HDxdwCh0pOwN8LDi3CauEs2h4cwEnz1ysa+Z9ZiguQ/6+AMYOW4NgAJd1J6kv5x1NegS0oZ9X2X+a6R9DTWJFuY0aVAtuf/9d/srR8zJnMAYH1jJUk1NP6DqQ3/DCDsaK1lazqR+OjWkCDs2Y3dCEroW5Q2IM1H8gR4nXQO9XTMbo9IpvV1ENp8ogGlJa7xTCOh9drMfNXrex92NZdnrSlNsx/9b9M73tMuH3+0ZfNytL0dPOzCSkYQthVZN5wmtgTzjW2UOtBiCGmbeGPO5/ZlBJbnmV3iwDpv1XiUvWLhJfdhsKk6VM9gXgcC8UExHpxeT88uhxG9F4QSKOKbgvhrh2imqRQiD64hvThBA0I98UjiddtCvDDUMm2v3ScAPUjl8SSiCzKYsfBLfnfgFYOa7VscK/+8Y3v8gOSHRWTCVb+lWm+nuAnBGk52IQT7kHV8U5L3b6L+r+nEVVHPKyNrpkhmCpM4+ld9e887oUIecCZrbOrpsYEdqCOjZyNyQh/A98cYDZdAOiztxJOnGr8tIwDFLryzRHz9tQ9AIAoadOT+NvAOzEDM+u2G6nSu7OsdQXzPU6vyFM3ItIqclfaiu7g8+9/58pqDh/NX8ds=", + "variants": [] + }, + { + "component": "server", + "platform": "edge1", + "file": "nxwitness-server_update-5.0.0.35270-edge1.zip", + "size": 85894365, + "md5": "b222e3008273335f17a421a58e8392ae", + "signature": "MlAJf3klqKYCVYR9Jf74bK5/ekicsqP56O5rqxBFzZ/g99ZZpFkI/2jTDSM7VcrY1fpjVTsyTSf0Tci7Mk+quB1bzYlU7UKLslmmsKwPxv1Ho3bYZBL2ufcyv/aljZNxg1OS9EihMmit3O/7w8KEnGRLWsToIB6VN737W/6wuAqfzBv6gBEjHyEDk3Vwi90BYXFgAP7I5NrN6ugcy94yzIFS7RuWsY6NWwW4O7NS8o/vr6N4szT3lYIC+rrMa0xoyB7yAxATTp/NIhWUZXV9cmAS0xulPt9bgXIcz8w7LtyHAOmdQbOnWcN11QIpwRrtTIykVmY5Cz2dfREa94IN1Rgu+/XrChhyvIIGfoY+qrG5co0sdAJQZEnvrjr3iVmyp/U4bx6ZL7VRLheHT/GGUvny7jW5jryS/cDQST+isXBSc8QOEK0dtgVhVwlLFT5cwvQ1E1Fa+sNYw7A8VQzRdjhvfZVUSIyEbrWOpy8mA2L2axrhXoKUOhvQ5d5/+vyjQumHUCDFuVXPabX6cr0hLzPV+Riz7u+R/yZncVV9V7OeIFtOw2rtgQ5NJb/mer4Z9230gzu/uFu6Gm1WGi31G4rxMUq2QklpEQRWrMGeOfzrDszQuVJc+XOWrBjLYDideqScDy728NGBWGomtsLePyF7d9ABMcmwa4U5yoVIfNY=", + "variants": [] + }, + { + "component": "server", + "platform": "linux_arm64", + "file": "nxwitness-server_update-5.0.0.35270-linux_arm64.zip", + "size": 146000167, + "md5": "af4dcd0463070803191c759bb9c2cc24", + "signature": "jmiKRBpvkEdPPOzQhp1efSY4l1vjx8hZEJrO9Ariyp1c8kgdERAnDmNQZzRcdekcQKJtNwi757UOZI/UcxWJp+TNyDQwfERs1Fk1JB/cSX0flYzcBMIdVe4LAFBoBDE/3Asu+hYYOZFP1QYqB2Am7uRRMMUtvhe59U384ykdiM+ueJ35MKV7FnzF4uj1/b80UBJTb3CmFocxvgwK1+aZjoFpSWMJj1DqJ7WRBKL+wwdfN11DprRw6eMUqc4mL7ddRnIjCWAJr8vXGOZnMT/+rXxizhcQTJqrxL/OUGa3kUqNzyhyDyOZfO1vxyg86hUZ+ztRmu0ekuizbAXjQEq6zmQN5teXyBf6t4UJrq7kjaZWSaS4czelYQyLe6ikFjUMM/ROIagwpjACfhR/NLo313hEzIrd/r3cnA/uWdKfjCs22kP7U+c99fno/LW4Z1LgX5/O34iHGveHs30Gigv33Iv0u8+CEqTZsWSUTcbflVwpk4JCUf0XzpC48n+rqP4wAbcauPQTWVXC+O+EUjS4O+zjJL5Vl1f4IGTwegwBYi4OceOJiZYFgIxV6JTgDVvtLRvn3+u7ec2KvOS0v4U6cBhJPOs66Qf3O0Wvm9/NaYpNr183F9r1n88rPekFBgGrAycSgJszvnibtCq//3S7nTKBSAtf9zwYMsYLaGeMxkA=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "16.04" + } + ] + }, + { + "component": "server", + "platform": "linux_arm32", + "file": "nxwitness-server_update-5.0.0.35270-linux_arm32.zip", + "size": 169700173, + "md5": "e591ad3c572bb08c077e1189817ef9fb", + "signature": "M6u0J+rxn0+D/J7sA6sT8cPMvhxynRMLY+IX1CZKne1gQSY1VwsZAYjev25L0HGULM7Pyvuzzwykgbn4rsSieps/mNays0RAjRUVOTKMs1ZndZ7iQy6oitDdpxF5iU8Zy1mYyf3idFJmDRUlu7YlmzLHkVmIoAFCReEISPceNA/r1IWQRzC5povzUPk5JcQA0Kv3KL4Hbo6dgNCk3gZR/KaGCC6mPygQTkVjeXOQn3Ci8tH6DS8lzlwOR5heLGjm7+akQ8ETCksbC2zrHpnSQCQVJGfjwkCrG/LhBcjDOST6ucga8Q1HGqHseXYF7xi6s7/3OwpKKydVvQ6mtkxXHL8PVlqjRCdcQCHyJpB38S2V3n4dXagfzROS4GNvgFS3mq6Br87HBZDoQSta0yH9lwscwnIh6V3SRSpY1e8/4kzERXTkYKqAeoy7naMKqGSVzHEUb9wRolvl6oZhiLkjntk8txyj2YkpLb8ZZjTfU99PwenIVwn8nfNvsso02UoBFqZXRWOu84qmSWf7W48f6A38KaXVvdjtEwFsBeTQe9wRc+lHH4kLNGQ3RQCyTm7eC8QcJsCjNAk4SJDCeBCH2i77mno3Mug38CmqzZQLmjjbY5dMLgepowQpjqXKm6f5UbyTkreaShVu4YMWTGNuk41o4LDYkafavSfFKxl1Hh0=", + "variants": [ + { + "name": "debian", + "minimumVersion": "9" + }, + { + "name": "ubuntu", + "minimumVersion": "16.04" + }, + { + "name": "raspbian", + "minimumVersion": "9" + } + ] + }, + { + "component": "client", + "platform": "linux_x64", + "file": "nxwitness-client_update-5.0.0.35270-linux_x64.zip", + "size": 271453787, + "md5": "d1276fd672b9c32a1b0ed7b7529763ac", + "signature": "eGpTGO4Gg+WCgqOAcPBbuSdCM1Hfv46AlYWI1qZIa2KHax9B47VEdo3wSHdhcyMjjXxn3mavU5zIVNOcx7b5ovr0qQKMBC3OBV/ZWnp2iInB8LKd8VjvMk7sBiegaBTgdDqxHIeXNTlE2bvIVdhPalWHZeUPILzfWdtdOhE4sO4Csej8ZGrT8TkcleVG12/gZMjXa3Wof8FpXt95994+0RpxP/Ec4Z8IHxZP0nN0e3zLF7/m6z305wgrTEUfpMlYOQbb9d/7O5KPEcK6IjIOHQ+9VlczdYAzuXpz2WqR6UOzbtG+cKLTuiAEjLjvRpGRowYmVGpL3R2vCp47EsFrWveFpYUaBgsO+/Wz2X+E4veN3GY3uM/MZmNncOsROYxJ1MDV2AFkMjB/6lfzwcND+/q6IHLL3APPoYLXicvH29A+NBZhudy6AqZ2M6t3Uhj8VHvYz/vU+uN8CDsvozv/eid+do9oVzFd0iZcMfXHjRCbg2YjXdtTdrfkYLkCip0Yx3PbP8GZWs7Z8G6jmr8iZZxmt03aotraXWGvjtFcKQOKM+r+d9irDNNsYiuf2AvQGCOkwl4EnQSoxyWpGpqHISOFX8VbOYPXmhbdEr+8aJmArMzcVj0QVXdaSWEZsqbJ4yfRXFfx0ipaTSptlcBTl5M6kEBEWcTRFCRlPEzNCKs=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "16.04" + } + ] + }, + { + "component": "client", + "platform": "windows_x64", + "file": "nxwitness-client_update-5.0.0.35270-windows_x64.zip", + "size": 156180611, + "md5": "2462fb805a75275244ebac1b1f349472", + "signature": "CJsqytA3QieL/yQJ7fQEQXdgPI+CnbkHTjVArcm+kXOCtzLsehx/j9z4vsjhOilXQTY2mYPe+44OkziNq6GwPoj3Vd3CxmWep8pdySFPu5CJdM3aDPByvTbz2On+Ps+/HUXReroMmB0Jx/KHaU0bNBhXz2GjauYp+XmAbY2eCQrPc6W0l7LO7/nD0Rs7oM8YDtqZ5m/2p4UgaXzeencETyNUFgYc3Vixj+RRI8lHj29/oLS/f8l6/dnXKi3M5G9iuqBRvHvZc7CY+iTImPxNO6QbPbktnZEKNCtgh9kTTjp5ekGokk75R90kheoOrl+Z8U6T9G4mlmpK6SbSb3N8TCNNhesXucD0m2O0akWIER/m6B/hFdZ6AbjvwJZNIL1YMMh9wWluwsfCaVfgyihPVQHS25EkeZ8iGMjvfxWhZA4ADhk89Nbx0K32+CSBZjT70Jk8v6A3U5wTMFQHVFYFeH/nfCN1rHcJU6YYZr70iKGOG7qcN6QMvWn8/oadHGO7BanK9HXfy2o8KSNm0joT2XVwDBZwyfNdrTDGEDp3JVz5vW+dXVtywbBvNVm2l1OmZeKGHx7n9BhPhRga3VlntZESNUtrQQE91VtNb7bGn4YB7sHYXCg1M4OwmZ7cpuBuwbVL4adJ4JAYy3AhtqPCLi0NZhcguVJBz1+obbcHEAs=", + "variants": [] + }, + { + "component": "client", + "platform": "macos_x64", + "file": "nxwitness-client_update-5.0.0.35270-macos_x64.zip", + "size": 160431892, + "md5": "27ccd9decc6e12bb278d2445eef7a96a", + "signature": "aaj7K0xoWA5QsebnWtOQZnZw47Vh5GGXJlhazaCpkl1gnyaQ/EybuQg2m+v19KqoM+MtqFrbkh13fQVFvKNZ7ynPVz+EDQ6nTvblkKt01IFo9jzO4Gfvnxycq7prMl6kf8abg2Tf3Krhnrkm1Pcv+qYVkte1fgbF7umrD2sfSXbs7KxjKJV2Zeh2NJfKLrmshLU0YVjjEHmjwpasVzX6hDcQ7xavWxGIOf6uvZKgbENH2x8AiOlhbumkl1NLhD/PmWIOndE8buD8Erhy5cNlbO0JCxserF/ly6+NqwhneRXRWfMCHD/acdtrkK6GYWr+JfTmSdkmqwqGLRbOPDKeSneLrbc+RXse30XzlyaD/hzM2X3mvtaPtlfuaj5lnKg0AsYaxNL5QZ68lQDDA6mW/rJnMjl2nz7K/gKdmTlgd2sAjjfGQILaH+TiYKhTGv4gjurdfmrhUxY89w873XbLUZYsb1D/aoINXczpp8gmu7cEFw8BVgEjkNbvoME98OEGxllH8X1vtpB5/7jqVvhKtiHUJ8Y/TgfvMWwtD+KEVV6C2cH3+RsJpDIIDlux8/GNXHzmUUqdRS832nL4jgbSbePXmvJI4yGHZbbCZ1UM6TePtsDGvpAb83I1Rj+KjsBkPZppgwVdMY5jDXkFyDso/MF5QAZHzD5zTySZtu8fzIs=", + "variants": [] + }, + { + "component": "client", + "platform": "macos", + "file": "nxwitness-client_update-5.0.0.35270-macos.zip", + "size": 160431888, + "md5": "a0a7ec56f0efdec004491379354130b6", + "signature": "brtTWDXlH2OLY5D51vus8XEvyWuJqaL+rw4CVdiD18ms3w/OXTO+QHtHmRwbiNFm9zLzgiEM1snhQkIyLFJEC24XnYjwt2BxbWtdK1BOUzwJdNiglIkHtQQpYeVM5NK1teu78AMtKnuF5cKJNZ9J4fYuQqPhOYqLnXNaa9Sy2gq/B/P9kp21r58ov4hAYxSjrOTALIoyKLGQlUE452NXj1ng4t4ueujKrwT19gwq17hx4G5N5DEwrMmrbh2VLANl7I0FyyjwWo6I65Cn+AzAV/wXadoflguCEUMeS12mDzmZrd31l7yVd4SSvgbfdNNOaPWW5RgmjsJ9B+e5NQqB6JH99T8Y0CCvF1x+K9mtz4xtyQ7xBvZZudZSVQBACvMfzKgd/YgLPdEao72MwOpE2KZj5Rke9l+hDImcDPD/TEold26QMZHO3qgspKwmgTLVUrijSThuCCFvc57o1N8w8rruPC0pFyqJbexOTrnrGBMcEFFQ9XaIA5uc3/BN99TwOt2GddPpK5aXOxdTymMjB3kCxrw8sVO9zKk4P7sFYTIT1STuB9/2mCKy/s3DdmPENbiuzypE8LXmChIaecJK4R+XJuwx2fPgICg6Yp6M9QRiqXeFf/XevSXugZnbcyW0lpWwbzBVXPP/BftNp8+BTojsQb1TppZxxByb6k7j9vE=", + "variants": [] + }, + { + "component": "client", + "platform": "macos_arm64", + "file": "nxwitness-client_update-5.0.0.35270-macos_arm64.zip", + "size": 146991991, + "md5": "0f6741c39c00d5c7af8d9b13a25f8fcf", + "signature": "J66YBALqFUTKGYcHMe3FoDzvgcePvO1+uWY2963e1HMJMgSZ8gow9uqN2aV1wxN+Cja6vF0RHJ6JPmyIT2nH/TgGbDo4wG5N3xN2Dpt6CdVY9cF0ClMlnZYbZ8+o/NtR9bX78cEt0EK4BEHfKP1djYnjaBjBlH2s1ihciCwhxNt1vst9MpPiZK2k2+SFp2W1DnnvKXuwntECOaPfNBgBpzdap3EmZnRCQ8oOTGbgbz8pfn9YM4Rg7ZNVf8XWzSldMpuSHmS17dmnNobGF87T7WDV5VgShHrMmMoRsx7yvSS3zN1OD7fL0oEIREuBa191QXvc22T1icN6AXr03MESyy0HD8N2L6SVpoStq+dfRtRSO4NJX+26bcsxoMI4Ddhl/XAz88tGfW2o1vQiS3vx6UaXt/Uaw8OZoYsJREqtYsHSf2rUSMhbR76eD3P3apSp01rAzCcI4ZCwuJAJDlzWNr1oRNGzOSQFGhaRebqJNZINN5xswSbbPxyMSOD7x0MkFHP4Du77I6MvxROwzqCv+jB18En/KblazOGOhS4WP8NAbtV5s21nxerU5CbuMQl8CuuMi8DgwmuveYOsN0XXdk4F/B5SArJszEewqXj1BVfKYmTgJlbSJoF5XS8Hq77S+YispsHR/IaBHxe5M14X4pYK4yCmJA/IN6lBm18QOKk=", + "variants": [] + }, + { + "component": "client", + "platform": "linux_arm64", + "file": "nxwitness-client_update-5.0.0.35270-linux_arm64.zip", + "size": 276050812, + "md5": "83bac919b14dbf30d471b0746f666e30", + "signature": "HXGNqsprAgu+nzs2EJ/NB6ymjsTlphXXyw/ZWnEXQgghtoRqwkPuK2aOfV++gfN+SyzDFfDsC78ELfVD8ExoPlS7FHRpMCBuN3cKcVopFVRoaVuzNwObzsrkxE4A36ZwDqIE9c+lfDNsI8VMuBsEDm3kVscP4du2aqtGeFnc277JMKJIHzPfdWNO2Z5dptGiou/+86EGJAIUrQwon4vWkrZxSC4dg0nOXtQ9rhHhLQg1ArJ4+n9IyNdSbyzSBfGBX6HB1n7GNCWv5JgHRzXJyiS6CGr2HyE6SL8x1FmMV1s2UqNzjX5Co4sp61RYREatVtKvBIDwEIb+G1IsWKriaX7tW/DCKE9QeA4TtzMDPKmhg8LvxBiNmTsY7ZlUDwbZ2nD9lo/0DJmvMEUXxAMAYXSQPX74qu24YSMQWOgBODeypNsqQrRLlpCTnC4t1VS+cYdD8dlwm6Q5485TraJ3lZzTlISUTjuI1tLiJGZmnQklFTcW+hOHjoyo/AdKgG/MmK34jEtKoRbY0d6jRQiAryEHa70M7dYu038Ulz7Hls/nsDAo4RdCAx85LJZeunH4hifnqt/Z5dcFBuP0FNuKLghy+8cBG585HqhKj0qs7PSNWUa1azEPLJq1AoaaOaWTHBlJm81HocY42XHYx13rLfNK46eqIqhAvcsreClg18U=", + "variants": [ + { + "name": "ubuntu", + "minimumVersion": "16.04" + } + ] + }, + { + "component": "server", + "platform": "nx1", + "file": "", + "size": 0, + "md5": "", + "variants": [ + { + "name": "", + "minimumVersion": "3000" + } + ] + } + ] } \ No newline at end of file diff --git a/Samples/Releases/DWSpectrum.1.json b/Samples/Releases/DWSpectrum.1.json index 6996433d..406024d1 100644 --- a/Samples/Releases/DWSpectrum.1.json +++ b/Samples/Releases/DWSpectrum.1.json @@ -1,16 +1,16 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/digitalwatchdog", - "http://beta.networkoptix.com/beta-builds/digitalwatchdog" - ], - "releases": [ - { - "product": "vms", - "version": "5.0.0.35271", - "protocol_version": 5002, - "publication_type": "release", - "release_date": "1660167578448", - "release_delivery_days": null - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/digitalwatchdog", + "http://beta.networkoptix.com/beta-builds/digitalwatchdog" + ], + "releases": [ + { + "product": "vms", + "version": "5.0.0.35271", + "protocol_version": 5002, + "publication_type": "release", + "release_date": "1660167578448", + "release_delivery_days": null + } + ] } \ No newline at end of file diff --git a/Samples/Releases/DWSpectrum.2.json b/Samples/Releases/DWSpectrum.2.json index 5d44fec6..640f8380 100644 --- a/Samples/Releases/DWSpectrum.2.json +++ b/Samples/Releases/DWSpectrum.2.json @@ -1,24 +1,24 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/digitalwatchdog", - "http://beta.networkoptix.com/beta-builds/digitalwatchdog" - ], - "releases": [ - { - "product": "vms", - "version": "5.0.0.35138", - "protocol_version": 5002, - "publication_type": "release", - "release_date": null, - "release_delivery_days": null - }, - { - "product": "vms", - "version": "4.2.0.32842", - "protocol_version": 4201, - "publication_type": "release", - "release_date": "1627938214069", - "release_delivery_days": 30 - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/digitalwatchdog", + "http://beta.networkoptix.com/beta-builds/digitalwatchdog" + ], + "releases": [ + { + "product": "vms", + "version": "5.0.0.35138", + "protocol_version": 5002, + "publication_type": "release", + "release_date": null, + "release_delivery_days": null + }, + { + "product": "vms", + "version": "4.2.0.32842", + "protocol_version": 4201, + "publication_type": "release", + "release_date": "1627938214069", + "release_delivery_days": 30 + } + ] } \ No newline at end of file diff --git a/Samples/Releases/DWSpectrum.3.json b/Samples/Releases/DWSpectrum.3.json index a934068f..248640c9 100644 --- a/Samples/Releases/DWSpectrum.3.json +++ b/Samples/Releases/DWSpectrum.3.json @@ -1,16 +1,16 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/digitalwatchdog", - "http://beta.networkoptix.com/beta-builds/digitalwatchdog" - ], - "releases": [ - { - "product": "vms", - "version": "5.0.0.35271", - "protocol_version": 5002, - "publication_type": "release", - "release_date": "1662033178345", - "release_delivery_days": null - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/digitalwatchdog", + "http://beta.networkoptix.com/beta-builds/digitalwatchdog" + ], + "releases": [ + { + "product": "vms", + "version": "5.0.0.35271", + "protocol_version": 5002, + "publication_type": "release", + "release_date": "1662033178345", + "release_delivery_days": null + } + ] } \ No newline at end of file diff --git a/Samples/Releases/DWSpectrum.4.json b/Samples/Releases/DWSpectrum.4.json index 331dab41..5aff8080 100644 --- a/Samples/Releases/DWSpectrum.4.json +++ b/Samples/Releases/DWSpectrum.4.json @@ -1,24 +1,24 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/digitalwatchdog", - "https://beta.networkoptix.com/beta-builds/digitalwatchdog" - ], - "releases": [ - { - "product": "vms", - "version": "5.0.0.35138", - "protocol_version": 5002, - "publication_type": "release", - "release_date": null, - "release_delivery_days": null - }, - { - "product": "vms", - "version": "4.2.0.32842", - "protocol_version": 4201, - "publication_type": "release", - "release_date": "1627938214069", - "release_delivery_days": 30 - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/digitalwatchdog", + "https://beta.networkoptix.com/beta-builds/digitalwatchdog" + ], + "releases": [ + { + "product": "vms", + "version": "5.0.0.35138", + "protocol_version": 5002, + "publication_type": "release", + "release_date": null, + "release_delivery_days": null + }, + { + "product": "vms", + "version": "4.2.0.32842", + "protocol_version": 4201, + "publication_type": "release", + "release_date": "1627938214069", + "release_delivery_days": 30 + } + ] } \ No newline at end of file diff --git a/Samples/Releases/DWSpectrum.5.json b/Samples/Releases/DWSpectrum.5.json index 406afbbb..2c6e4d95 100644 --- a/Samples/Releases/DWSpectrum.5.json +++ b/Samples/Releases/DWSpectrum.5.json @@ -1,32 +1,32 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/digitalwatchdog", - "https://beta.networkoptix.com/beta-builds/digitalwatchdog" - ], - "releases": [ - { - "product": "vms", - "version": "5.1.0.36751", - "protocol_version": 5107, - "publication_type": "rc", - "release_date": null, - "release_delivery_days": null - }, - { - "product": "vms", - "version": "5.0.0.36634", - "protocol_version": 5005, - "publication_type": "release", - "release_date": null, - "release_delivery_days": null - }, - { - "product": "vms", - "version": "4.2.0.32842", - "protocol_version": 4201, - "publication_type": "release", - "release_date": "1627938214069", - "release_delivery_days": 30 - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/digitalwatchdog", + "https://beta.networkoptix.com/beta-builds/digitalwatchdog" + ], + "releases": [ + { + "product": "vms", + "version": "5.1.0.36751", + "protocol_version": 5107, + "publication_type": "rc", + "release_date": null, + "release_delivery_days": null + }, + { + "product": "vms", + "version": "5.0.0.36634", + "protocol_version": 5005, + "publication_type": "release", + "release_date": null, + "release_delivery_days": null + }, + { + "product": "vms", + "version": "4.2.0.32842", + "protocol_version": 4201, + "publication_type": "release", + "release_date": "1627938214069", + "release_delivery_days": 30 + } + ] } \ No newline at end of file diff --git a/Samples/Releases/DWSpectrum.json b/Samples/Releases/DWSpectrum.json index ce20dba8..e30c89e7 100644 --- a/Samples/Releases/DWSpectrum.json +++ b/Samples/Releases/DWSpectrum.json @@ -1,24 +1,24 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/digitalwatchdog", - "http://beta.networkoptix.com/beta-builds/digitalwatchdog" - ], - "releases": [ - { - "product": "vms", - "version": "5.0.0.35271", - "protocol_version": 5002, - "publication_type": "release", - "release_date": null, - "release_delivery_days": null - }, - { - "product": "vms", - "version": "4.2.0.32842", - "protocol_version": 4201, - "publication_type": "release", - "release_date": "1627938214069", - "release_delivery_days": 30 - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/digitalwatchdog", + "http://beta.networkoptix.com/beta-builds/digitalwatchdog" + ], + "releases": [ + { + "product": "vms", + "version": "5.0.0.35271", + "protocol_version": 5002, + "publication_type": "release", + "release_date": null, + "release_delivery_days": null + }, + { + "product": "vms", + "version": "4.2.0.32842", + "protocol_version": 4201, + "publication_type": "release", + "release_date": "1627938214069", + "release_delivery_days": 30 + } + ] } \ No newline at end of file diff --git a/Samples/Releases/NxMeta.json b/Samples/Releases/NxMeta.json index 7cf308d1..cc2402f0 100644 --- a/Samples/Releases/NxMeta.json +++ b/Samples/Releases/NxMeta.json @@ -1,24 +1,24 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/metavms", - "http://beta.networkoptix.com/beta-builds/metavms" - ], - "releases": [ - { - "product": "vms", - "version": "5.1.0.35151 R1", - "protocol_version": 5105, - "publication_type": "beta", - "release_date": null, - "release_delivery_days": null - }, - { - "product": "vms", - "version": "5.0.0.35269 R11", - "protocol_version": 5002, - "publication_type": "release", - "release_date": "1659433159847", - "release_delivery_days": null - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/metavms", + "http://beta.networkoptix.com/beta-builds/metavms" + ], + "releases": [ + { + "product": "vms", + "version": "5.1.0.35151 R1", + "protocol_version": 5105, + "publication_type": "beta", + "release_date": null, + "release_delivery_days": null + }, + { + "product": "vms", + "version": "5.0.0.35269 R11", + "protocol_version": 5002, + "publication_type": "release", + "release_date": "1659433159847", + "release_delivery_days": null + } + ] } \ No newline at end of file diff --git a/Samples/Releases/NxWitness.2.json b/Samples/Releases/NxWitness.2.json index 17184de5..90e204db 100644 --- a/Samples/Releases/NxWitness.2.json +++ b/Samples/Releases/NxWitness.2.json @@ -1,24 +1,24 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/default", - "https://beta.networkoptix.com/beta-builds/default" - ], - "releases": [ - { - "product": "vms", - "version": "5.1.0.36751", - "protocol_version": 5107, - "publication_type": "rc", - "release_date": null, - "release_delivery_days": null - }, - { - "product": "vms", - "version": "5.0.0.36634", - "protocol_version": 5005, - "publication_type": "release", - "release_date": "1682031478805", - "release_delivery_days": 7 - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/default", + "https://beta.networkoptix.com/beta-builds/default" + ], + "releases": [ + { + "product": "vms", + "version": "5.1.0.36751", + "protocol_version": 5107, + "publication_type": "rc", + "release_date": null, + "release_delivery_days": null + }, + { + "product": "vms", + "version": "5.0.0.36634", + "protocol_version": 5005, + "publication_type": "release", + "release_date": "1682031478805", + "release_delivery_days": 7 + } + ] } \ No newline at end of file diff --git a/Samples/Releases/NxWitness.json b/Samples/Releases/NxWitness.json index 08b2e2bb..8163ccec 100644 --- a/Samples/Releases/NxWitness.json +++ b/Samples/Releases/NxWitness.json @@ -1,24 +1,24 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/default", - "http://beta.networkoptix.com/beta-builds/default" - ], - "releases": [ - { - "product": "vms", - "version": "5.0.0.35270", - "protocol_version": 5002, - "publication_type": "release", - "release_date": null, - "release_delivery_days": null - }, - { - "product": "vms", - "version": "4.2.0.32840", - "protocol_version": 4201, - "publication_type": "release", - "release_date": "1620848664362", - "release_delivery_days": 30 - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/default", + "http://beta.networkoptix.com/beta-builds/default" + ], + "releases": [ + { + "product": "vms", + "version": "5.0.0.35270", + "protocol_version": 5002, + "publication_type": "release", + "release_date": null, + "release_delivery_days": null + }, + { + "product": "vms", + "version": "4.2.0.32840", + "protocol_version": 4201, + "publication_type": "release", + "release_date": "1620848664362", + "release_delivery_days": 30 + } + ] } \ No newline at end of file diff --git a/Samples/Releases/Nxwitness.1.json b/Samples/Releases/Nxwitness.1.json index 6621703c..528aee62 100644 --- a/Samples/Releases/Nxwitness.1.json +++ b/Samples/Releases/Nxwitness.1.json @@ -1,16 +1,16 @@ -{ - "packages_urls": [ - "https://updates.networkoptix.com/default", - "http://beta.networkoptix.com/beta-builds/default" - ], - "releases": [ - { - "product": "vms", - "version": "5.0.0.35270", - "protocol_version": 5002, - "publication_type": "release", - "release_date": "1659371734147", - "release_delivery_days": null - } - ] +{ + "packages_urls": [ + "https://updates.networkoptix.com/default", + "http://beta.networkoptix.com/beta-builds/default" + ], + "releases": [ + { + "product": "vms", + "version": "5.0.0.35270", + "protocol_version": 5002, + "publication_type": "release", + "release_date": "1659371734147", + "release_delivery_days": null + } + ] } \ No newline at end of file diff --git a/Unraid/DWSpectrumLSIO.xml b/Unraid/DWSpectrumLSIO.xml index 3bee4c47..f3749bdd 100644 --- a/Unraid/DWSpectrumLSIO.xml +++ b/Unraid/DWSpectrumLSIO.xml @@ -1,85 +1,85 @@ - - - True - DWSpectrumLSIO - ptr727/dwspectrum-lsio - https://hub.docker.com/r/ptr727/dwspectrum-lsio/ - https://github.com/ptr727/NxWitness - MIT - https://github.com/ptr727/NxWitness - DW Spectrum IPVMS Docker based on LinuxServer - DW Spectrum IPVMS Docker based on LinuxServer - HomeAutomation: Security: Status:Beta - http://[IP]:[PORT:7001] - https://github.com/ptr727/NxWitness/Unraid - https://github.com/ptr727/NxWitness/raw/main/Unraid/DWLogo.png - - - /mnt/user/appdata/dwspectrum-lsio - /config - rw - - - /mnt/user - /media - rw - - - - - 99 - PUID - - - - 100 - PGID - - - - - 99 - 100 - /mnt/user/appdata/dwspectrum-lsio - /mnt/user - + + + True + DWSpectrumLSIO + ptr727/dwspectrum-lsio + https://hub.docker.com/r/ptr727/dwspectrum-lsio/ + https://github.com/ptr727/NxWitness + MIT + https://github.com/ptr727/NxWitness + DW Spectrum IPVMS Docker based on LinuxServer + DW Spectrum IPVMS Docker based on LinuxServer + HomeAutomation: Security: Status:Beta + http://[IP]:[PORT:7001] + https://github.com/ptr727/NxWitness/Unraid + https://github.com/ptr727/NxWitness/raw/main/Unraid/DWLogo.png + + + /mnt/user/appdata/dwspectrum-lsio + /config + rw + + + /mnt/user + /media + rw + + + + + 99 + PUID + + + + 100 + PGID + + + + + 99 + 100 + /mnt/user/appdata/dwspectrum-lsio + /mnt/user + diff --git a/Unraid/NxMetaLSIO.xml b/Unraid/NxMetaLSIO.xml index 1edb7899..457bee4c 100644 --- a/Unraid/NxMetaLSIO.xml +++ b/Unraid/NxMetaLSIO.xml @@ -1,85 +1,85 @@ - - - True - NxMetaLSIO - ptr727/nxmeta-lsio - https://hub.docker.com/r/ptr727/nxmeta-lsio/ - https://github.com/ptr727/NxWitness - MIT - https://github.com/ptr727/NxWitness - NxMeta VMS Docker based on LinuxServer - NxMeta VMS Docker based on LinuxServer - HomeAutomation: Security: Status:Beta - http://[IP]:[PORT:7001] - https://github.com/ptr727/NxWitness/Unraid - https://github.com/ptr727/NxWitness/raw/main/Unraid/NxLogo.png - - - /mnt/user/appdata/nxmeta-lsio - /config - rw - - - /mnt/user - /media - rw - - - - - 99 - PUID - - - - 100 - PGID - - - - - 99 - 100 - /mnt/user/appdata/nxmeta-lsio - /mnt/user - + + + True + NxMetaLSIO + ptr727/nxmeta-lsio + https://hub.docker.com/r/ptr727/nxmeta-lsio/ + https://github.com/ptr727/NxWitness + MIT + https://github.com/ptr727/NxWitness + NxMeta VMS Docker based on LinuxServer + NxMeta VMS Docker based on LinuxServer + HomeAutomation: Security: Status:Beta + http://[IP]:[PORT:7001] + https://github.com/ptr727/NxWitness/Unraid + https://github.com/ptr727/NxWitness/raw/main/Unraid/NxLogo.png + + + /mnt/user/appdata/nxmeta-lsio + /config + rw + + + /mnt/user + /media + rw + + + + + 99 + PUID + + + + 100 + PGID + + + + + 99 + 100 + /mnt/user/appdata/nxmeta-lsio + /mnt/user + diff --git a/Unraid/NxWitnessLSIO.xml b/Unraid/NxWitnessLSIO.xml index da50c289..0afe0fe7 100644 --- a/Unraid/NxWitnessLSIO.xml +++ b/Unraid/NxWitnessLSIO.xml @@ -1,85 +1,85 @@ - - - True - NxWitnessLSIO - ptr727/nxwitness-lsio - https://hub.docker.com/r/ptr727/nxwitness-lsio/ - https://github.com/ptr727/NxWitness - MIT - https://github.com/ptr727/NxWitness - NxWitness VMS Docker based on LinuxServer - NxWitness VMS Docker based on LinuxServer - HomeAutomation: Security: Status:Beta - http://[IP]:[PORT:7001] - https://github.com/ptr727/NxWitness/Unraid - https://github.com/ptr727/NxWitness/raw/main/Unraid/NxLogo.png - - - /mnt/user/appdata/nxwitness-lsio - /config - rw - - - /mnt/user - /media - rw - - - - - 99 - PUID - - - - 100 - PGID - - - - - 99 - 100 - /mnt/user/appdata/nxwitness-lsio - /mnt/user - + + + True + NxWitnessLSIO + ptr727/nxwitness-lsio + https://hub.docker.com/r/ptr727/nxwitness-lsio/ + https://github.com/ptr727/NxWitness + MIT + https://github.com/ptr727/NxWitness + NxWitness VMS Docker based on LinuxServer + NxWitness VMS Docker based on LinuxServer + HomeAutomation: Security: Status:Beta + http://[IP]:[PORT:7001] + https://github.com/ptr727/NxWitness/Unraid + https://github.com/ptr727/NxWitness/raw/main/Unraid/NxLogo.png + + + /mnt/user/appdata/nxwitness-lsio + /config + rw + + + /mnt/user + /media + rw + + + + + 99 + PUID + + + + 100 + PGID + + + + + 99 + 100 + /mnt/user/appdata/nxwitness-lsio + /mnt/user + diff --git a/WORKFLOW.md b/WORKFLOW.md index 26bd88a9..8db3ffab 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -1,638 +1,639 @@ -# WORKFLOW.md - -The single guide for this repo's CI/CD **workflows** (GitHub Actions): **code style**, **architecture**, a -**behavioral contract** (expected inputs and outputs), and a **test methodology**. Source code style lives -in [`CODESTYLE.md`](./CODESTYLE.md). This file covers everything under -[`.github/workflows/`](./.github/workflows/). - -It **describes required outcomes, not a required implementation.** A workflow is correct when it satisfies -the contract (section 4), whatever shape its YAML takes. Section 2 keeps workflows legible. Section 3 is -the model. Section 4 is what they must *do*. Sections 5 and 6 are how to verify it and the configuration it -assumes. Each guarantee names the **failure it prevents**, so the reason survives a reimplementation. - -## 0. The model at a glance - -NxWitness ships **Docker images only**: a multi-product, multi-base set of multi-arch images on Docker Hub -(five Nx VMS products x {plain, LSIO} = ten product images, plus two shared base images `nx-base` / -`nx-base-lsio`, across 12 Docker Hub repositories). There is **no NuGet publish**: the .NET project -(`CreateMatrix`) is a build-time **matrix generator** only, not a shipped package. Three things do the work: - -- **CI** runs on **push to every branch**: it validates (unit tests + lint) and smoke-builds a representative - image subset, publishing nothing. A pull request merges only when its required check is green. -- **The publisher** is a **triggered-Docker** workflow, one run = one branch. It runs on a **weekly schedule** - (rebuilds `main` only - full product matrix, shared base refresh for CVEs, versioned release), on a - **path-scoped push to `main`** when codegen commits a new `Make/Matrix.json` pin (publishes the new product - versions at once), and on **manual dispatch** (publishes the branch it is started from: `main` -> - `latest`/`stable`, `develop` -> `:develop`). It never runs on an ordinary merge. -- **Codegen** runs daily, dual-targeting `main` AND `develop`: it regenerates `Make/Version.json` + - `Make/Matrix.json` from upstream Nx product versions and opens a PR against each base, which the merge-bot - auto-merges. A merge to `main` whose matrix changed triggers the publisher's path-scoped push. - -There is no publish-on-merge for code and no two-branch matrix in the publisher - building only the trigger -branch keeps `github.ref` aligned with the branch being versioned. Dependabot pull requests merge themselves -once their checks pass. - -### Glossary - -- **Entry workflow** - has `push` / `schedule` / `workflow_dispatch` triggers. The orchestrator that an event - or a person starts. -- **Reusable workflow (task)** - a `workflow_call` workflow invoked through a `uses:` reference, never - triggered directly. File ends in `-task.yml`. -- **Product image** - one shipped image built from a `Make/Matrix.json` row's Dockerfile (e.g. `NxMeta`, - `NxMeta-LSIO`), pushed to its own Docker Hub repo (`docker.io/ptr727/`). -- **Shared base image** - `nx-base` / `nx-base-lsio`, built once and reused as the `FROM` for the product - images. Its tag (`:ubuntu-noble`) is **branch-agnostic** (one shared tag, not per-branch). -- **Matrix / version inputs** - `Make/Matrix.json` (the per-product image build rows: name, Dockerfile tags, - build args, branch) and `Make/Version.json` (the upstream Nx version pins), both regenerated by codegen. -- **Smoke build** - a CI build that compiles a representative image subset (NxMeta + NxMeta-LSIO, amd64) to - prove the Dockerfiles still build, publishing and pushing nothing. Driven by a `smoke: true` input. -- **Transfer artifact** - a workflow artifact handing data between jobs of one run. The durable copy lives on - the GitHub release / Docker Hub. -- **Threaded version** - the single NBGV `SemVer2` (plus `GitCommitId`) computed once in `get-version-task` - and passed down as `semver2` / `ref` inputs to every consumer, never recomputed in a build task. -- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted from - the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). The merge-bot and the codegen - PR-opener use it, not `GITHUB_TOKEN`: a `GITHUB_TOKEN` push does not trigger downstream workflows, and that - token is read-only on Dependabot pull requests. - -## 1. Purpose and how to use this document - -- **Contract, not implementation.** Conform to the *outcomes* in section 4 and the *architecture* in section - 3. Job names and file layout may vary; the input/output behavior may not. -- **"Operational" - the one definition.** The repo is **operational** when every applicable section-4 - guarantee holds, every applicable section-5B scenario's observed output equals its expected output - (corroborated by a 5C live probe where a live signal exists), and the section-6 configuration is in place. - Anything else is **not operational**. -- **Defect vs N/A.** An item is **N/A** only when this repo has no such concern (e.g. a fork-PR scenario, - since a fork cannot push here, or a NuGet-publish guarantee, since this repo publishes none). A construct - required by an applicable guarantee but absent is a **defect**. -- **Default branch is `main`.** Guarantees say "default branch" portably. This repo writes the literal `main` - in the publish guards and the release-version backstop, and the anchored `^refs/heads/main$` in - `version.json`'s `publicReleaseRefSpec`. - -## 2. Workflow style conventions - -Legibility rules. Necessary but not sufficient: a perfectly styled workflow can still violate section 4. - -- **Action pinning.** Pin every action to a commit SHA with a trailing `# vX.Y.Z` comment. Use `# vX` only - when the upstream floating major tag has no specific patch SHA. **Sole exception: `dotnet/nbgv@master`** is - consumed via the floating `@master` ref, never SHA-pinned - its tag stream lags `master` substantially, so - Dependabot tag-tracking would only propose downgrades to stale tags. The rationale lives in an inline - comment in [`get-version-task.yml`](./.github/workflows/get-version-task.yml); leave that comment intact. A - tool an action *installs* (not a `uses:` ref) is left unpinned to track latest. -- **Filename.** Reusable workflows end in `-task.yml`; entry workflows end in what they do - (`-pull-request.yml`, `-release.yml`). A `-task.yml` is `uses:`-d, never triggered directly. -- **Workflow `name:`.** Reusable names end in **"task"**, entry names in **"action"**. -- **Job and step `name:`.** Every job `name:` ends in **"job"**, every step `name:` in **"step"**, the - aggregator included (`Check pull request workflow status job`). A job name also bound as a ruleset - required-check `context:` is codified in [`repo-config/`](./repo-config/) and changed only **in lockstep** - with the live ruleset. -- **Concurrency.** Every entry workflow declares a `concurrency` group. CI uses - `group: '${{ github.workflow }}-${{ github.ref }}'`, `cancel-in-progress: true`. The publisher overrides - it: a ref-independent group with `cancel-in-progress: false`, so two publishes never overlap (a schedule, a - dispatch, and a `Make/Matrix.json` pin push, or back-to-back dispatches against the shared Docker tags) and - none is cancelled mid-release. Codegen keys on the workflow only (it writes fixed `codegen-*` branches). -- **Shells.** Every multi-line bash `run:` starts with `set -euo pipefail`. -- **Conditionals.** Multi-line `if:` uses the folded scalar `if: >-`. -- **Boolean inputs.** A boolean used by both `workflow_call` and `workflow_dispatch` is declared in both - trigger blocks and compared against `true` and `'true'`. -- **Reusable-workflow permissions.** Job-level `permissions:` are validated before `if:`, so even a skipped - job needs valid permissions. Grant least privilege; a callee's extra scope is granted by the caller. -- **Allowlist `success` and `skipped` explicitly** across an optional dependency: use - `(needs.X.result == 'success' || needs.X.result == 'skipped')`, not `!= 'failure'`. -- **Line endings.** Workflow YAML follows [`.editorconfig`](./.editorconfig) (CRLF). Preserve on every edit. - -## 3. Architecture - -### Three workflows: CI on push, publishing on schedule/pin-push/dispatch, codegen daily - -CI ([`test-pull-request.yml`](./.github/workflows/test-pull-request.yml)) and the publisher -([`publish-release.yml`](./.github/workflows/publish-release.yml)) are separate workflows with separate -concurrency, so they never race. CI re-tests every pushed tree and never publishes; the publisher releases on -its own triggers and never runs on an ordinary merge. Codegen -([`run-periodic-codegen-pull-request.yml`](./.github/workflows/run-periodic-codegen-pull-request.yml) -> -[`run-codegen-pull-request-task.yml`](./.github/workflows/run-codegen-pull-request-task.yml)) keeps the -version/matrix data current. *Prevents a merge from silently cutting a release, and a CI run from racing a -publish on the same ref.* - -### The publisher builds one branch: the trigger ref - -A publish builds exactly **one** branch - the run's trigger ref. The **schedule** and the **pin push** both -run on `main`; a **dispatch** runs on the branch it is started from (`main` or `develop`). The jobs pass -`github.ref_name` as both `ref` and `branch`, so the branch built, versioned, and tagged is always the run's -own ref. *No matrix and no cross-branch ref mixing - `github.ref` is the branch being published.* The jobs -are guarded to the long-lived branches (`main` / `develop`); a stray dispatch from a feature branch is a -no-op. To refresh `:develop`, dispatch the workflow from `develop`. - -Because the run's ref **is** the built branch, GitHub resolves the local `uses: ./...` reusable workflows -from that same branch's commit - so a `develop` dispatch runs develop's own task definitions, and the -schedule runs main's. - -### The publisher's pin-push trigger - -The publisher's `push` trigger is branch-filtered to `main` and path-filtered to `Make/Matrix.json`. So an -ordinary code merge never publishes; only a change to the codegen matrix pin (committed to `main` by the -auto-merged `codegen-main` PR) does, shipping the new upstream Nx product versions immediately. develop's -daily codegen pin update is sync-only - the push trigger is main-only - and never publishes; `:develop` is -refreshed by manual dispatch. The weekly schedule still runs with no matrix change, to refresh the shared -base image for CVEs. - -### The multi-image build layer - -The publisher decomposes into a single `get-version` -> `build-base` -> `build-docker` -> -`github-release` -> `docker-readme` -> `cleanup-artifacts` chain (a multi-product Docker repo, not the -template's single-target branch matrix). [`build-base-images-task.yml`](./.github/workflows/build-base-images-task.yml) -builds the two shared bases; [`build-docker-task.yml`](./.github/workflows/build-docker-task.yml) builds the -product matrix from `Make/Matrix.json` (`max-parallel: 4`). The shared base is built **once** (on the `main` -run) and reused: a `develop` dispatch sets `build_base: false` and pulls main's published base, so it never -overwrites the branch-agnostic `nx-base` tag. The product build reads both branches' registry buildcaches -(`buildcache-main`, `buildcache-develop`) and writes only its own branch's cache, only when pushing. - -### Versioning: compute once, thread everywhere - -NBGV runs once (in [`get-version-task.yml`](./.github/workflows/get-version-task.yml)), classifying from -`github.ref`, and its outputs (`SemVer2`, `GitCommitId`) thread to every consumer via `outputs:` / `needs:` / -`semver2` inputs. `build-docker-task` accepts the threaded `semver2` as the image `LABEL_VERSION` and never -re-runs NBGV - one classification feeds every product leg, so no second NBGV run can reclassify or collide a -tag. A build job may check out a specific commit to compile it (main pins to `GitCommitId`) but consumes the -threaded version. `main` (the public ref, `publicReleaseRefSpec = ^refs/heads/main$`) builds a clean -`X.Y.`; every other branch a prerelease `X.Y.-g`. *Keeps each image's embedded version -and the release tag in agreement.* - -NBGV classifies `publicReleaseRefSpec` from the `GITHUB_REF` environment variable. Because the publisher -builds the **trigger ref** (one branch per run), `GITHUB_REF` already equals the branch being versioned - a -schedule, a pin push, or a `main` dispatch classifies as public; a `develop` dispatch as prerelease - so no -`GITHUB_REF` override is needed (`IGNORE_GITHUB_REF` is only for matrix publishers that build a non-trigger -branch). The main-version backstop (D2.2) catches any misclassification. - -NBGV's version is the GitHub release tag on `main` and the `LABEL_VERSION` build arg baked into the images. -The Docker image **tags** themselves carry the Nx product version from `Make/Matrix.json`, distinct from the -NBGV build version. - -### Validate at entry - -A run that carries a cross-input invariant (`main` must not carry a prerelease suffix) asserts it once with -`::error::` before the release is published. The `github-release` job `needs:` the version job and runs the -backstop step first. - -### Fast CI feedback, head-resolved - -CI runs on push to every branch, so GitHub head-resolves the reusable `./...` workflows from the pushed head: -a pull request that edits a reusable task tests its own copy. CI validates (the reusable `validate-task`: -Husky lint + `dotnet test`) on every push, and smoke-builds a representative image subset only when image -files changed (an inline `git diff` change-gate, no `dorny/paths-filter`), uploading and pushing nothing. One -aggregator job, the ruleset-bound required check, gates the merge. A branch-deletion push (all-zeros -`github.sha`) is skipped by a `!github.event.deleted` guard on every job, so a deletion never runs a failing -build. - -### The Docker-only release - -The `github-release` job tags the built commit and creates the GitHub release (auto source zip + README + -LICENSE; `target_commitish` pinned to `GitCommitId`; skip-existing guard; main-only). This repo ships **no** -`release-asset-*` binaries or packages - the published artifacts are the Docker Hub images - so there is no -release-asset download step and `fail_on_unmatched_files` is omitted. The GitHub release exists only as the -version anchor / tag. The Docker Hub repository overview (the repo `README.md`) is pushed to every product + -base repo on a `main` Docker publish (the `docker-readme` jobs), since Docker Hub does not read the GitHub -README; the repo list is derived inline from `Make/Matrix.json`. - -### Resource lifecycle - -Workflow artifacts are an intra-run handoff; the durable copy lives on the GitHub release / Docker Hub. The -publisher and CI both run a terminal `cleanup-artifacts` job that deletes the run's transfer artifacts so -they do not accumulate against the small account-wide storage quota; it is `continue-on-error` so housekeeping -never reds the run. - -### Self-sufficiency: automatic updates - -Every Dependabot pull request, any ecosystem and any tier, auto-merges once the required checks pass, except -a **semver-major NuGet** bump, which waits for human review. Codegen opens a `codegen-main` -> `main` and a -`codegen-develop` -> `develop` PR daily; the merge-bot auto-merges each independently (`--delete-branch`). -A merged dependency bump does not itself publish; a merged matrix pin on `main` does (the pin-push trigger). -A person steps in only for a breaking change (a red check) or to dispatch a release. - -### Flow diagrams - -Four diagrams trace the architecture above: the pull-request gate, the self-publisher, the bot -automation, and the trigger chain that turns a daily codegen run into a published release. They depict -the same outcomes that the section 4 contract specifies, drawn from the workflow YAML; if a diagram and -a guarantee disagree, one of them is a defect. Triggers are blue, gates yellow, durable/published -outputs green, and stop/skip outcomes red. - -**Pull request (CI) - `test-pull-request.yml`.** Every push head-resolves the reusable tasks, runs the -validate gate, smoke-builds a representative image subset only when image files changed, and a single -aggregator produces the ruleset-bound required check (D1, D6). - -```mermaid -flowchart TD - T(["push: every branch
(or workflow_dispatch)"]):::trig - T --> D{"github.event.deleted?"}:::gate - D -- "yes: branch deletion" --> X(["all jobs + aggregator skip
no failed run, no pending check"]):::stop - D -- "no" --> CH["changes job
inline git diff change-gate
image? base?"] - D -- "no" --> V["validate job
(validate-task.yml)"] - subgraph VT ["validate-task.yml"] - VU["Husky lint (CSharpier,
dotnet format style)
+ dotnet test"] - end - V --> VT - CH --> SG{"image files changed?
(Docker/**, Make/Matrix.json, Make/Version.json)"}:::gate - SG -- "no" --> SS(["smoke-build skipped
(aggregator allows skip)"]):::stop - SG -- "yes" --> S["smoke-build job
build-docker-task.yml
smoke: true, push: false
NxMeta + NxMeta-LSIO, amd64"] - CH --> A - VT --> A - S --> A - SS --> A - A{"Check pull request workflow status job
changes AND validate AND smoke-build
succeeded or skipped?"}:::gate - A -- "yes" --> G(["required check passes
merge unblocked"]):::pub - A -- "no" --> R(["required check fails
merge blocked"]):::stop - classDef trig fill:#dbeafe,stroke:#2563eb,color:#1e3a8a - classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 - classDef pub fill:#dcfce7,stroke:#16a34a,color:#14532d - classDef stop fill:#fee2e2,stroke:#dc2626,color:#7f1d1d -``` - -**Publish - `publish-release.yml`.** A weekly schedule (main), a `Make/Matrix.json` pin push (main), or a -dispatch versions once with NBGV, builds the shared base (main only), validates, builds the 12-image -product matrix with the threaded SemVer2, then cuts the main-only GitHub release and refreshes the Docker -Hub overviews (D0, D2, D3, D4). - -```mermaid -flowchart TD - P1(["schedule: weekly Mon 02:00 UTC
(main only)"]):::trig --> GV - P2(["push: main
paths = Make/Matrix.json (codegen pin)"]):::trig --> GV - P3(["workflow_dispatch
(main or develop)"]):::trig --> GV - GV{"get-version job
ref_name in (main, develop)?"}:::gate - GV -- "feature branch" --> GVS(["all jobs skip
no publish"]):::stop - GV -- "yes" --> GVR["get-version job
(get-version-task.yml)
NBGV @master, runs once
SemVer2 + GitCommitId"] - GVR --> BB{"ref_name == main?"}:::gate - BB -- "develop dispatch" --> BBS(["build-base skipped
reuse main's nx-base"]):::stop - BB -- "main" --> BBJ["build-base job
(build-base-images-task.yml)
nx-base + nx-base-lsio
amd64 + arm64, branch ref (github.ref_name)"] - GVR --> VAL["validate job
(validate-task.yml)
main: pinned to GitCommitId"] - VAL --> BD - BBJ --> BD - BBS --> BD - BD["build-docker job
(build-docker-task.yml, build_base: false)
12-image matrix from Make/Matrix.json
amd64 + arm64, max-parallel 4
LABEL_VERSION = threaded SemVer2"] - BD --> DH[("Docker Hub
10 product repos (branch tags)
+ 2 shared base repos")]:::pub - BD --> RG{"ref_name == main?"}:::gate - RG -- "develop" --> RGS(["no GitHub release
(:develop images only)"]):::stop - RG -- "main" --> VPR{"github-release job
SemVer2 has no prerelease '-'?
(strip +buildmetadata)"}:::gate - VPR -- "prerelease suffix" --> VPRX(["fail ::error::
refuse to publish"]):::stop - VPR -- "clean" --> EX{"tag exists AND not dispatch?"}:::gate - EX -- "yes" --> EXS(["skip release create
(no-op republish)"]):::stop - EX -- "no" --> REL[("GitHub release
tag = SemVer2 at GitCommitId
prerelease: false, source zip + README + LICENSE")]:::pub - BD --> DRR["docker-readme-repos job
derive repo list from Matrix.json"] - DRR --> DRM["docker-readme job (matrix)
push README to each Docker Hub repo"] - DRM --> DRO[("Docker Hub overviews
10 product + 2 base repos")]:::pub - classDef trig fill:#dbeafe,stroke:#2563eb,color:#1e3a8a - classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 - classDef pub fill:#dcfce7,stroke:#16a34a,color:#14532d - classDef stop fill:#fee2e2,stroke:#dc2626,color:#7f1d1d -``` - -**Automation - codegen + Dependabot + merge-bot.** Daily codegen and Dependabot open in-repo bot PRs; the -merge-bot enables auto-merge (or disables it on a maintainer push); the required check gates each merge -(D8). - -```mermaid -flowchart TD - SCH(["schedule daily 04:00 UTC
(or workflow_dispatch)"]):::trig --> CG - subgraph CGT ["run-codegen-pull-request-task.yml (matrix: main, develop)"] - CG["codegen job per branch
regenerate Version.json + Matrix.json
(deterministic, forward-only guard)"] --> CGC{"data changed?"}:::gate - CGC -- "no" --> CGN(["no PR"]):::stop - CGC -- "yes" --> CPR["open codegen-<branch> PR
(App token)"] - end - DEP(["Dependabot opens PR
any ecosystem/tier"]):::trig --> MB - CPR --> MB - subgraph MBT ["merge-bot-pull-request.yml (pull_request_target)"] - MB{"event / author"}:::gate - MB -- "opened/reopened
bot author" --> EN["enable auto-merge --delete-branch
squash develop / merge main"] - MB -- "synchronize by maintainer" --> DIS["disable auto-merge"] - end - EN --> SM{"semver-major NuGet?"}:::gate - SM -- "yes" --> HUM(["wait for human review"]):::stop - SM -- "no" --> CK{"required check passes?"}:::gate - CK -- "yes" --> MRG(["PR merges (App token)"]):::pub - CK -- "no" --> BLK(["merge blocked
maintainer notified"]):::stop - MRG -. "codegen-main Matrix.json change" .-> PUBR(["publisher pin-push auto-publishes main"]):::pub - classDef trig fill:#dbeafe,stroke:#2563eb,color:#1e3a8a - classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 - classDef pub fill:#dcfce7,stroke:#16a34a,color:#14532d - classDef stop fill:#fee2e2,stroke:#dc2626,color:#7f1d1d -``` - -**Trigger chain - daily codegen to published release.** The recurring path that ships a new upstream Nx -product version with no human in the loop: only a `codegen-main` matrix change reaches the publisher's -pin push; the develop pin update is sync-only, and `:develop` / the weekly base refresh come from the -schedule and dispatch (D4.1, D8.3). - -```mermaid -flowchart TD - PER(["schedule daily 04:00 UTC
run-periodic-codegen-pull-request.yml
(or workflow_dispatch)"]):::trig --> CGM - CGM["codegen matrix: main + develop
regenerate Version.json + Matrix.json"] --> DCH{"Matrix.json changed?"}:::gate - DCH -- "no" --> NOPR(["no PR, nothing ships"]):::stop - DCH -- "yes: main" --> PRM["codegen-main -> main PR
(merge-bot auto-merges)"] - DCH -- "yes: develop" --> PRD["codegen-develop -> develop PR
(merge-bot auto-merges)"] - PRD --> SYNC(["develop Matrix.json updated
sync-only, push trigger is main-only
:develop refreshed by dispatch"]):::stop - PRM --> PUSH(["push to main
paths = Make/Matrix.json"]):::trig - PUSH --> PUB["publish-release.yml
pin-push: build base + 12-image matrix"] - PUB --> SINK[("Docker Hub product + base images
+ main GitHub release")]:::pub - SCHED(["weekly schedule (main)
base refresh for CVEs"]):::trig --> PUB - DISP(["workflow_dispatch (main or develop)
force publish that branch"]):::trig --> PUB - classDef trig fill:#dbeafe,stroke:#2563eb,color:#1e3a8a - classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 - classDef pub fill:#dcfce7,stroke:#16a34a,color:#14532d - classDef stop fill:#fee2e2,stroke:#dc2626,color:#7f1d1d -``` - -## 4. Behavioral contract - expected outcomes - -Each is a **MUST**, stated as input -> output plus the failure it prevents. - -### D0 - Architecture - -- **D0.1 CI is one run, one branch.** Input: any push. Output: `test-pull-request` builds/validates exactly - `github.ref_name` and publishes nothing. *Prevents cross-branch ref mixing in CI.* -- **D0.2 The publisher builds one branch: the trigger ref.** Output: the publisher passes `github.ref_name` - as `ref` and `branch`, so it checks out, versions, and tags exactly the run's own branch (the schedule/pin - push's `main`, or a dispatch's branch). No branch matrix; the jobs are guarded to `main`/`develop`. - *Prevents cross-branch ref mixing - `github.ref` is the branch being published.* -- **D0.3 One version, threaded.** Output: NBGV runs once (`get-version-task`); every consumer reads it via - `needs:` outputs / the `semver2` input; no consumer recomputes it. *Allowed:* checking out a specific - commit to compile it, and recording the built commit as the release `target_commitish`. *Prevents an - image's embedded version diverging from its tag, and a second NBGV run colliding image tags.* - -### D1 - CI fast feedback - -- **D1.1 Every push validates; image changes smoke-build.** Output: on any push the `validate` job (the - reusable `validate-task`) runs with no paths filter; `smoke-build` (NxMeta + NxMeta-LSIO, amd64, no push) - runs when the inline change-gate detects an image-file change (`Docker/**`, `Make/Matrix.json`, - `Make/Version.json`). *Prevents a reusable-workflow or Dockerfile break shipping untested.* -- **D1.2 Unit tests always run.** Output: `validate-task` runs `dotnet test` (the codegen tool + its tests). -- **D1.3 Lint enforces the editor checks in CI.** Output: `validate-task` runs Husky (`dotnet husky run`: - CSharpier + `dotnet format style --verify-no-changes`) - the same checks the editor and the pre-commit - hook run. Workflow YAML is lint-checked by `actionlint` (run from the editor / locally). -- **D1.4 Smoke never publishes and never uploads a release asset.** Output: a smoke build compiles the image - subset but makes no GitHub release and no Docker push (`push: false`, `smoke: true`). The Docker login runs - on every build including smoke (for higher pull/cache rate limits against the registry buildcache), so a - Dependabot-triggered push-CI smoke build needs the Docker Hub credentials in both secret stores; fork PRs do - not run this push-CI. -- **D1.5 One required aggregator gates merge.** Output: a single aggregator job must **succeed** (success or, - for the conditionally-skipped `smoke-build`, skipped; never failure/cancelled), `needs:` `changes`, - `validate`, and `smoke-build`, and treats a `changes` failure as blocking (so an image-changing PR cannot - merge on a `changes` failure mis-read as skip). Its name is ruleset-bound (D6.2) and must not be renamed. - -### D2 - Validation at entry - -- **D2.1 Validate the cross-input invariant before publishing.** Output: the `github-release` job asserts each - cross-input invariant with `::error::` (the main-version backstop, D2.2) before the release is created; - downstream steps `needs:` the version job. The publish run also re-runs `validate-task` (the `validate` job - gates `build-docker`), so a publish can never ship a tree that would fail the same lint + `dotnet test` gate - CI enforces on push - on the trigger branch CI already validated, re-checked at publish time. -- **D2.2 Main matches version classification.** Input: a real publish run for `main`. Output: the release - fails loudly if `main` carries a prerelease suffix. It strips `+buildmetadata` before testing for the - prerelease `-`. *Prevents a develop build published as the stable `latest`.* - -### D3 - Versioning and classification - -- **D3.1 NBGV runs once, threaded.** Output: NBGV runs once, classifying from the checked-out branch; no - consumer re-invokes it. The run builds the trigger ref, so `GITHUB_REF` already matches the branch being - versioned and NBGV classifies it correctly (no `IGNORE_GITHUB_REF`). The threaded `semver2` is the image - `LABEL_VERSION`. -- **D3.2 `main` = stable, others = prerelease.** Output: `main` -> `X.Y.Z`, any other branch -> - `X.Y.Z-g`. The release-version backstop names `main`; `publicReleaseRefSpec` is `^refs/heads/main$`. -- **D3.3 Version floor + git height.** Output: `version.json` sets the major.minor floor, NBGV appends the git - height as the patch, never bumped on a cadence. *(Who raises the floor and when is a human-process rule in - `AGENTS.md`.)* - -### D4 - Release / publish - -- **D4.1 Publish only on schedule, the matrix-pin push, or dispatch - never on an ordinary merge.** Output: - `publish-release` triggers are `schedule` (weekly), `workflow_dispatch`, and a `push` **branch-filtered to - `main` and path-filtered to `Make/Matrix.json`**. There is no other `push` trigger and no `PUBLISH_ON_MERGE` - variable. The jobs are guarded to `github.ref_name` in (`main`, `develop`), so a stray dispatch from a - feature branch is a no-op. *Prevents per-merge release churn while still shipping a new product pin at once.* -- **D4.2 A publish builds the one trigger branch in full.** Output: the run builds the shared base (main run - only) + the full product matrix and creates the GitHub release for `github.ref_name` - the schedule/pin push - rebuilds `main` (stable / `latest`); a dispatch publishes its own branch (`main` stable / `latest`, - `develop` prerelease / `:develop`, no GitHub release). *Prevents a half-published image set and cross-branch - ref mixing.* -- **D4.3 Tag the built commit.** Output: the release `target_commitish` is the run's `GitCommitId` (the tip of - `main`), and main's images are built from that pinned commit, never the moving ref. *Prevents the tag / - embedded version landing on the wrong commit.* -- **D4.4 Release contents and gate.** Output: a release is a tag on the built commit plus the auto source zip, - README, and LICENSE - no binary release assets (`fail_on_unmatched_files` omitted; the images are the - artifact). The release is `main`-only (`github.ref_name == 'main'`); `prerelease: false`. *Prevents - publishing a develop build as a stable GitHub release.* -- **D4.5 No-op republish.** Input: a weekly re-run whose version is unchanged. Output: the release-create step - is skipped when the tag already exists (refreshed only on `workflow_dispatch`), while the Docker push still - runs - re-pushing the same tags refreshes the shared base image. *Prevents duplicate releases while still - refreshing the images.* -- **D4.6 Publish is built from the tree CI validated.** Output: the run's ref **is** the published branch, so - the reusable-task definitions and the built tree resolve from that branch - the same tree CI validated on - push (the required check gates every merge to it) with the identical `validate-task` definition - and the publish run re-runs that `validate-task` (the - `validate` job gates `build-docker`). The main-version backstop (D2.2) is the additional in-publisher gate. -- **D4.7 Docker publishing authenticates with Docker Hub credentials.** Output: the base and product builds - log in via `docker/login-action` with `DOCKER_HUB_USERNAME` + `DOCKER_HUB_ACCESS_TOKEN` and push with - `docker/build-push-action`; the Docker Hub overview is pushed with the same token. There is **no NuGet/OIDC - publishing** in this repo. *Prevents a missing-credential publish failure.* -- **D4.8 Branch-scoped Docker buildcache.** Output: the base and product builds read both branches' registry - caches (`buildcache-main`, `buildcache-develop`) and write only their own branch's cache, only when pushing, - so a `main` and a `develop` publish never overwrite each other's cache. *Prevents one branch's publish - destroying the other's cache hit-rate.* -- **D4.9 Multi-arch, multi-product, shared-base fan-out.** Output: the publish builds every product image from - `Make/Matrix.json` for `linux/amd64` + `linux/arm64`, on the shared base built once and reused; each product - image pushes to its own Docker Hub repo. *Prevents a per-arch or per-product gap, and a redundant per-branch - base rebuild overwriting the shared tag.* - -### D5 - Resource cleanup - -- **D5.1 Terminal cleanup, best-effort.** The publisher and CI each run a terminal `cleanup-artifacts` job - (`always()`, `continue-on-error`) that deletes the run's transfer artifacts, independent of the required - aggregator so housekeeping never gates the merge. -- **D5.2 Never red the run on cleanup.** Cleanup failures warn, never fail. - -### D6 - Self-testing workflows - -- **D6.1 A change is testable on its own branch.** Output: a workflow or build change is exercised by CI on - the branch that introduces it, no dependency on reaching `main` first. -- **D6.2 Head-resolution, single producer, fork exception.** Output: CI runs on `push` to every branch so - reusable `./...` logic resolves from the head, and the aggregator's ruleset-bound `context:` is produced by - that push run as the sole producer of that name. Dependabot and codegen PRs are in-repo branches, validated - the same way. A fork cannot push, so it has no run and is validated by maintainer action - the one - exception. *Prevents a dual-producer context race and a false self-test claim for forks.* - -### D7 - Concurrency, permissions, safety - -- **D7.1 The publisher does not cancel mid-flight.** Output: ref-independent group, `cancel-in-progress: - false`. CI uses the `...-${{ github.ref }}` group with `cancel-in-progress: true`; the merge-bot keys on PR - number (D8.1); codegen keys on the workflow. -- **D7.2 Skipped jobs still need valid permissions.** Output: every reusable job runs under valid least-privilege - `permissions:`; a callee's extra scope is granted by the caller. -- **D7.3 Boolean inputs both forms.** Declared in both trigger blocks, compared against `true` and `'true'`. -- **D7.4 Branch-deletion guard.** Output: every CI job carries `if: !github.event.deleted` and the aggregator - `always() && !github.event.deleted`, so a branch-deletion push (all-zeros `github.sha`) runs no failing - build and does not fail the required check. - -### D8 - Bots and automation - -- **D8.1 Merge-bot.** Output: runs on `pull_request_target`, holds the App token, merges the PR by URL without - checking out its code. Enables auto-merge on `opened`/`reopened` with `--delete-branch`; squash on - `develop`, merge-commit on `main` by the PR's base ref; disables auto-merge when a maintainer pushes to a - bot branch (no `--delete-branch` on the disable path). Concurrency keyed on PR number. -- **D8.2 Dependabot auto-merges on green, semver-major NuGet excepted.** Output: every Dependabot PR - auto-merges once the required checks pass, except a semver-major NuGet bump (human review). A failing check - blocks the merge. A merged dependency bump does **not** itself publish (it ships in the next scheduled run - or the next matrix-pin push). -- **D8.3 Codegen dual-targets `main` AND `develop`.** Output: the daily codegen matrix opens a - `codegen-main` -> `main` and a `codegen-develop` -> `develop` PR (strict head/base pairing in the merge-bot), - regenerating `Make/Version.json` + `Make/Matrix.json` **only** (not the Dockerfiles - those are a separate - human-driven `Make/Create.sh` path). The merge-bot auto-merges each independently. develop's matrix update - is sync-only; a merged `codegen-main` matrix change fires the publisher's pin push. *Prevents non-linear - rebase/merge-block conflicts (the rejected single-target scheme) and keeps both branches current with - upstream Nx versions.* -- **D8.4 Forward-only version guard.** Output: the codegen tool's forward-only guard (`ReleaseVersionForward`, - covered by `CreateMatrixTests`) prevents a regenerated matrix from regressing a product version. *Prevents a - generic-tag downgrade slipping in via codegen.* - -### D9 - Style, static, and dropped workflows (see section 2) - -- **D9.1** Every action SHA-pinned with a version comment (sole exception: `dotnet/nbgv@master`, whose tag - stream lags master so Dependabot tag-tracking would only propose downgrades to stale tags - a deliberate - documented float, rationale inline in `get-version-task.yml`); an installed-tool version is left unpinned to - track latest. -- **D9.2** File/workflow/job/step names follow the suffix rules; a ruleset-bound `context:` name moves only in - lockstep with `repo-config/`. -- **D9.3** Bash `run:` blocks start `set -euo pipefail`; multi-line `if:` uses `>-`. -- **D9.4** Line endings follow `.editorconfig`. -- **D9.5 No decorative / dropped workflows.** No date-badge (`build-datebadge-*`), no standalone docker-readme - task (folded into the publisher), no `PUBLISH_ON_MERGE` variable, no `dorny/paths-filter` (replaced by the - inline change-gate). Their presence is a defect to remove. -- **D9.6** Style is enforced in CI by `validate-task` (D1.3), from the same config files the editor and Husky - hook use. - -### D10 - Repository configuration - -- **D10.1 Required configuration is present.** Output: the secrets, branch rulesets, and repository settings - section 6 lists are all in place, codified in [`repo-config/`](./repo-config/) and audited by its - `configure.sh check` (the 5D audit). The detail is in section 6. - -## 5. Test methodology - -### 5A. Static audit (no execution) - -Read the workflow files plus `version.json` and `Make/Matrix.json` and assert the fact behind each applicable -guarantee with a `file:line` citation: - -- **D0:** CI has no branch matrix; the publisher passes `github.ref_name` as `ref`/`branch` and is guarded to - `main`/`develop`; NBGV invoked once in `get-version-task`, `build-docker-task` has no nested `get-version` - and consumes the `semver2` input; the run builds the trigger ref so `GITHUB_REF` matches the versioned - branch. -- **D1:** CI runs on `push` with no paths filter on `validate`; the inline `changes` gate sets `image`/`base`; - `smoke-build` is `smoke: true`, `push: false`, amd64 NxMeta subset; the aggregator `needs:` `changes` + - `validate` + `smoke-build`, blocks on non-success, treats `changes` failure as blocking. -- **D2:** the main release backstop checks the prerelease `-`, strips `+buildmetadata`; `validate-task` is the - shared gate. -- **D3:** `main` appears in the backstop and the release `prerelease: false`/guard; `publicReleaseRefSpec` is - `^refs/heads/main$`; `semver2` threads to `LABEL_VERSION`. -- **D4:** `publish-release` triggers are `schedule` + `workflow_dispatch` + `push` (branches `[main]`, paths - `[Make/Matrix.json]`) only (no other push, no `PUBLISH_ON_MERGE`); jobs guarded to `github.ref_name` in - (`main`, `develop`); `target_commitish` = `GitCommitId`; main pins `ref` to `GitCommitId`, develop reuses - the base (`build_base: false`); the build logs in with `DOCKER_HUB_*`; buildcache branch-scoped and - write-gated on push; release-create gated `exists == false || workflow_dispatch`; release + docker-readme - gated to `main`; the product matrix builds amd64+arm64 from `Make/Matrix.json`. -- **D5:** the publisher and CI each have a terminal `cleanup-artifacts` (`always()`, `continue-on-error`), - independent of the aggregator. -- **D6:** CI is `push` on every branch; the aggregator context has exactly one producer; no `pull_request` - trigger; every CI job has the `!github.event.deleted` guard. -- **D7:** the publisher group is ref-independent with `cancel-in-progress: false`; the merge-bot keys on PR - number; codegen keys on the workflow; CI uses the standard group + deletion guard; reusable jobs declare - permissions. -- **D8/D9:** the merge-bot runs on `pull_request_target` with the App token, keyed on PR number, both merge - jobs use `--delete-branch`; Dependabot auto-merge excepts semver-major NuGet only; codegen dual-targets - main+develop and regenerates only `Version.json`/`Matrix.json`; no date-badge, standalone docker-readme - task, `PUBLISH_ON_MERGE`, or `dorny/paths-filter`; actions SHA-pinned except `nbgv@master`; - names/shells/conditionals per section 2. - -### 5B. End-to-end trace scenarios (deterministic from the YAML) - -| # | Input | Expected output | Exercises | -| --- | --- | --- | --- | -| S1 | push touching `Docker/**` | `validate` + `smoke-build` (NxMeta amd64) run, **no push, no release**; aggregator success; no dangling artifacts | D0.1, D1 | -| S2 | push changing only docs | `validate` runs; the `changes` gate sets `image=false`; `smoke-build` skipped; aggregator success (skip allowed) | D1, D1.5 | -| S3 | push changing only `.github/workflows/**` | `validate` runs head-resolved; `smoke-build` skipped (no image files); aggregator success | D1.1, D6.1 | -| S4 | weekly `schedule` | builds + publishes `main` only: shared base refresh + full product matrix (amd64+arm64) + stable release + `latest`; `target_commitish` = main's SHA; develop untouched; no dangling artifacts | D4.1, D4.2, D4.9 | -| S5 | push to `main` changing `Make/Matrix.json` (codegen pin) | publishes `main` with the new product versions immediately | D4.1, D8.3 | -| S6 | `workflow_dispatch` from `develop` | builds + publishes `develop`: `:develop` images, prerelease classification, `build_base: false` (reuses main's base), **no GitHub release** | D4.1, D4.2, D3.2 | -| S7 | `workflow_dispatch` re-run on `main`, no new commits | release-create refreshed on dispatch (skipped on schedule if the tag exists); Docker re-pushed (base refresh); no duplicate release | D4.5 | -| S8 | `workflow_dispatch` from a feature branch | the `github.ref_name in (main, develop)` guard skips every job -> no publish | D4.1 | -| S9 | merged dependency bump (any) | not a matrix-pin change; merges don't publish -> **no release**; ships in the next scheduled run | D4.1, D8.2 | -| S10 | merged develop codegen PR (`Matrix.json` change on develop) | sync-only; the pin push is main-only -> **no publish** | D8.3 | -| S11 | PR with a CSharpier / format / unit-test failure | `validate` fails -> aggregator blocks the merge | D1.2, D1.3, D1.5 | -| S12 | `version.json` floor bump merged | merges don't publish -> no immediate release; the new floor ships in the next publish | D3.3, D4.1 | -| S13 | Dependabot semver-major NuGet bump | gated on human review -> does not auto-merge; other majors auto-merge on green | D8.2 | -| S14 | branch-deletion push | every CI job + the aggregator skip (`!github.event.deleted`) -> no failing required check | D7.4 | -| S15 | `develop` -> `main` promotion (merge commit) | the merge itself does not publish; if it changed `Matrix.json` the pin push publishes main, else the next schedule does | D4.1, D8.1 | - -### 5C. Live probe (where warranted, never publishing) - -- Open a trivial doc-only PR and confirm S2 (`validate` runs, `smoke-build` skipped, aggregator green, 0 - artifacts left); open an image-touching PR and confirm S1 (smoke-build runs amd64, nothing pushed). -- After a `main` publish (schedule, pin push, or dispatch) confirm a stable release (`isPrerelease == false`) - with multi-arch product images (`docker buildx imagetools inspect` shows amd64 + arm64) and the shared base - tags intact; after a `develop` dispatch confirm `:develop` images, prerelease classification, and that the - shared `nx-base:ubuntu-noble` tag was not overwritten. A re-run adds no duplicate release. Absent publish - rights, record indeterminate and rely on 5A/5B. -- Trigger codegen via dispatch: confirm both `codegen-main` -> `main` and `codegen-develop` -> `develop` PRs - open and the merge-bot auto-merges with branch deletion. - -### 5D. Configuration audit - -Run [`repo-config/configure.sh check`](./repo-config/). It confirms the listed secrets exist (in both -stores), the `main`/`develop` rulesets enforce the required merge method + status check + signed commits + -strict-off, and the repository settings are in place, exiting non-zero on drift. Secret *values* cannot be -read back, so it asserts the names exist (failing if it cannot query them). The GitHub App installation is a -best-effort check (a precise check needs app-level auth, so it notes rather than fails). The Docker Hub -token's validity and push scope across the 12 repositories are a manual checklist item. - -### Assessment - -Operational when every applicable 5A item passes, every applicable 5B scenario matches (corroborated by 5C -where a live signal exists), and 5D configuration is in place. Procedure: **Audit** (5A + 5D) -> **Trace** -(5B) -> **Probe** (5C, without publishing) -> **Verdict** with the failing guarantee(s) and the triggering -input for each. - -## 6. Repository configuration - -The workflows depend on configuration outside the YAML. A misconfiguration surfaces only as a failed run, so -the configuration is part of "operational" (D10; audit 5D). - -**Secrets.** - -- `DOCKER_HUB_USERNAME` / `DOCKER_HUB_ACCESS_TOKEN` - Docker Hub credentials the base and product builds log - in with to push the images and the repository overviews. Required in **both** the Actions and Dependabot - secret stores: a Dependabot-triggered push runs CI whose Docker smoke build logs in too (when an image file - changed), and that run gets the Dependabot store. The access token needs push scope on every product + - base repo under `docker.io/ptr727/` (derived from `Make/Matrix.json` - see `repo-config/README.md`). There - is no NuGet/OIDC publishing. -- `CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY` - the GitHub App credentials the merge-bot and the - codegen PR-opener mint the App token from. Required in **both** the Actions and Dependabot secret stores (a - Dependabot-triggered run gets the Dependabot store). The App must be installed on the repo with - `contents: write` and `pull_requests: write`. -- The built-in `GITHUB_TOKEN` needs no setup. **No `PUBLISH_ON_MERGE` variable is used.** - -**Branch rulesets.** - -- `main` - merge-commit merges only; requires the aggregator status check (`Check pull request workflow status - job`); requires signed commits; "require branches up to date before merging" is **off** (a forward-only - `develop` makes every post-release `main` tip unreachable from `develop`, so the strict check would fail - every release). -- `develop` - squash merges only (keeps history linear); requires the same status check; requires signed - commits; "up to date" is **off** (so same-batch bot PRs auto-merge in parallel). -- The required check's `context:` matches the aggregator job name verbatim (D6.2, D9.2). - -**Repository settings.** Auto-merge enabled; squash and merge-commit both allowed (each ruleset narrows its -branch to one); rebase off; auto-delete-on-merge **off** (so `main`/`develop` survive a promotion; the -merge-bot deletes bot branches explicitly with `--delete-branch`). Dependabot version **and** security updates -enabled. The GitHub App installed with the scopes above. - -**Validation.** This configuration is codified in [`repo-config/`](./repo-config/) and applied/audited by -`repo-config/configure.sh`; `check` is the 5D audit. Secret values cannot be read back, so the audit asserts -the names exist (failing if they cannot be queried); the App installation is a best-effort check. +# WORKFLOW.md + +The single guide for this repo's CI/CD **workflows** (GitHub Actions): **code style**, **architecture**, a +**behavioral contract** (expected inputs and outputs), and a **test methodology**. Source code style lives +in [`CODESTYLE.md`](./CODESTYLE.md). This file covers everything under +[`.github/workflows/`](./.github/workflows/). + +It **describes required outcomes, not a required implementation.** A workflow is correct when it satisfies +the contract (section 4), whatever shape its YAML takes. Section 2 keeps workflows legible. Section 3 is +the model. Section 4 is what they must *do*. Sections 5 and 6 are how to verify it and the configuration it +assumes. Each guarantee names the **failure it prevents**, so the reason survives a reimplementation. + +## 0. The model at a glance + +NxWitness ships **Docker images only**: a multi-product, multi-base set of multi-arch images on Docker Hub +(five Nx VMS products x {plain, LSIO} = ten product images, plus two shared base images `nx-base` / +`nx-base-lsio`, across 12 Docker Hub repositories). There is **no NuGet publish**: the .NET project +(`CreateMatrix`) is a build-time **matrix generator** only, not a shipped package. Three things do the work: + +- **CI** runs on **push to every branch**: it validates (unit tests + lint) and smoke-builds a representative + image subset, publishing nothing. A pull request merges only when its required check is green. +- **The publisher** is a **triggered-Docker** workflow, one run = one branch. It runs on a **weekly schedule** + (rebuilds `main` only - full product matrix, shared base refresh for CVEs, versioned release), on a + **path-scoped push to `main`** when codegen commits a new `Make/Matrix.json` pin (publishes the new product + versions at once), and on **manual dispatch** (publishes the branch it is started from: `main` -> + `latest`/`stable`, `develop` -> `:develop`). It never runs on an ordinary merge. +- **Codegen** runs daily, dual-targeting `main` AND `develop`: it regenerates `Make/Version.json` + + `Make/Matrix.json` from upstream Nx product versions and opens a PR against each base, which the merge-bot + auto-merges. A merge to `main` whose matrix changed triggers the publisher's path-scoped push. + +There is no publish-on-merge for code and no two-branch matrix in the publisher - building only the trigger +branch keeps `github.ref` aligned with the branch being versioned. Dependabot pull requests merge themselves +once their checks pass. + +### Glossary + +- **Entry workflow** - has `push` / `schedule` / `workflow_dispatch` triggers. The orchestrator that an event + or a person starts. +- **Reusable workflow (task)** - a `workflow_call` workflow invoked through a `uses:` reference, never + triggered directly. File ends in `-task.yml`. +- **Product image** - one shipped image built from a `Make/Matrix.json` row's Dockerfile (e.g. `NxMeta`, + `NxMeta-LSIO`), pushed to its own Docker Hub repo (`docker.io/ptr727/`). +- **Shared base image** - `nx-base` / `nx-base-lsio`, built once and reused as the `FROM` for the product + images. Its tag (`:ubuntu-noble`) is **branch-agnostic** (one shared tag, not per-branch). +- **Matrix / version inputs** - `Make/Matrix.json` (the per-product image build rows: name, Dockerfile tags, + build args, branch) and `Make/Version.json` (the upstream Nx version pins), both regenerated by codegen. +- **Smoke build** - a CI build that compiles a representative image subset (NxMeta + NxMeta-LSIO, amd64) to + prove the Dockerfiles still build, publishing and pushing nothing. Driven by a `smoke: true` input. +- **Transfer artifact** - a workflow artifact handing data between jobs of one run. The durable copy lives on + the GitHub release / Docker Hub. +- **Threaded version** - the single NBGV `SemVer2` (plus `GitCommitId`) computed once in `get-version-task` + and passed down as `semver2` / `ref` inputs to every consumer, never recomputed in a build task. +- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted from + the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). The merge-bot and the codegen + PR-opener use it, not `GITHUB_TOKEN`: a `GITHUB_TOKEN` push does not trigger downstream workflows, and that + token is read-only on Dependabot pull requests. + +## 1. Purpose and how to use this document + +- **Contract, not implementation.** Conform to the *outcomes* in section 4 and the *architecture* in section + 3. Job names and file layout may vary; the input/output behavior may not. +- **"Operational" - the one definition.** The repo is **operational** when every applicable section-4 + guarantee holds, every applicable section-5B scenario's observed output equals its expected output + (corroborated by a 5C live probe where a live signal exists), and the section-6 configuration is in place. + Anything else is **not operational**. +- **Defect vs N/A.** An item is **N/A** only when this repo has no such concern (e.g. a fork-PR scenario, + since a fork cannot push here, or a NuGet-publish guarantee, since this repo publishes none). A construct + required by an applicable guarantee but absent is a **defect**. +- **Default branch is `main`.** Guarantees say "default branch" portably. This repo writes the literal `main` + in the publish guards and the release-version backstop, and the anchored `^refs/heads/main$` in + `version.json`'s `publicReleaseRefSpec`. + +## 2. Workflow style conventions + +Legibility rules. Necessary but not sufficient: a perfectly styled workflow can still violate section 4. + +- **Action pinning.** Pin every action to a commit SHA with a trailing `# vX.Y.Z` comment. Use `# vX` only + when the upstream floating major tag has no specific patch SHA. **Sole exception: `dotnet/nbgv@master`** is + consumed via the floating `@master` ref, never SHA-pinned - its tag stream lags `master` substantially, so + Dependabot tag-tracking would only propose downgrades to stale tags. The rationale lives in an inline + comment in [`get-version-task.yml`](./.github/workflows/get-version-task.yml); leave that comment intact. A + tool an action *installs* (not a `uses:` ref) is left unpinned to track latest. +- **Filename.** Reusable workflows end in `-task.yml`; entry workflows end in what they do + (`-pull-request.yml`, `-release.yml`). A `-task.yml` is `uses:`-d, never triggered directly. +- **Workflow `name:`.** Reusable names end in **"task"**, entry names in **"action"**. +- **Job and step `name:`.** Every job `name:` ends in **"job"**, every step `name:` in **"step"**, the + aggregator included (`Check pull request workflow status job`). A job name also bound as a ruleset + required-check `context:` is codified in the hub's repository-configuration payloads and changed only + **in lockstep** with the live ruleset. +- **Concurrency.** Every entry workflow declares a `concurrency` group. CI uses + `group: '${{ github.workflow }}-${{ github.ref }}'`, `cancel-in-progress: true`. The publisher overrides + it: a ref-independent group with `cancel-in-progress: false`, so two publishes never overlap (a schedule, a + dispatch, and a `Make/Matrix.json` pin push, or back-to-back dispatches against the shared Docker tags) and + none is cancelled mid-release. Codegen keys on the workflow only (it writes fixed `codegen-*` branches). +- **Shells.** Every multi-line bash `run:` starts with `set -Eeuo pipefail`. +- **Conditionals.** Multi-line `if:` uses the folded scalar `if: >-`. +- **Boolean inputs.** A boolean used by both `workflow_call` and `workflow_dispatch` is declared in both + trigger blocks and compared against `true` and `'true'`. +- **Reusable-workflow permissions.** Job-level `permissions:` are validated before `if:`, so even a skipped + job needs valid permissions. Grant least privilege; a callee's extra scope is granted by the caller. +- **Allowlist `success` and `skipped` explicitly** across an optional dependency: use + `(needs.X.result == 'success' || needs.X.result == 'skipped')`, not `!= 'failure'`. +- **Line endings.** Workflow YAML is LF, per [`.editorconfig`](./.editorconfig)'s `[*]` default (Actions and Dependabot rewrite it that way). Preserve endings on every edit. + +## 3. Architecture + +### Three workflows: CI on push, publishing on schedule/pin-push/dispatch, codegen daily + +CI ([`test-pull-request.yml`](./.github/workflows/test-pull-request.yml)) and the publisher +([`publish-release.yml`](./.github/workflows/publish-release.yml)) are separate workflows with separate +concurrency, so they never race. CI re-tests every pushed tree and never publishes; the publisher releases on +its own triggers and never runs on an ordinary merge. Codegen +([`run-periodic-codegen-pull-request.yml`](./.github/workflows/run-periodic-codegen-pull-request.yml) -> +[`run-codegen-pull-request-task.yml`](./.github/workflows/run-codegen-pull-request-task.yml)) keeps the +version/matrix data current. *Prevents a merge from silently cutting a release, and a CI run from racing a +publish on the same ref.* + +### The publisher builds one branch: the trigger ref + +A publish builds exactly **one** branch - the run's trigger ref. The **schedule** and the **pin push** both +run on `main`; a **dispatch** runs on the branch it is started from (`main` or `develop`). The jobs pass +`github.ref_name` as both `ref` and `branch`, so the branch built, versioned, and tagged is always the run's +own ref. *No matrix and no cross-branch ref mixing - `github.ref` is the branch being published.* The jobs +are guarded to the long-lived branches (`main` / `develop`); a stray dispatch from a feature branch is a +no-op. To refresh `:develop`, dispatch the workflow from `develop`. + +Because the run's ref **is** the built branch, GitHub resolves the local `uses: ./...` reusable workflows +from that same branch's commit - so a `develop` dispatch runs develop's own task definitions, and the +schedule runs main's. + +### The publisher's pin-push trigger + +The publisher's `push` trigger is branch-filtered to `main` and path-filtered to `Make/Matrix.json`. So an +ordinary code merge never publishes; only a change to the codegen matrix pin (committed to `main` by the +auto-merged `codegen-main` PR) does, shipping the new upstream Nx product versions immediately. develop's +daily codegen pin update is sync-only - the push trigger is main-only - and never publishes; `:develop` is +refreshed by manual dispatch. The weekly schedule still runs with no matrix change, to refresh the shared +base image for CVEs. + +### The multi-image build layer + +The publisher decomposes into a single `get-version` -> `build-base` -> `build-docker` -> +`github-release` -> `docker-readme` -> `cleanup-artifacts` chain (a multi-product Docker repo, not the +template's single-target branch matrix). [`build-base-images-task.yml`](./.github/workflows/build-base-images-task.yml) +builds the two shared bases; [`build-docker-task.yml`](./.github/workflows/build-docker-task.yml) builds the +product matrix from `Make/Matrix.json` (`max-parallel: 4`). The shared base is built **once** (on the `main` +run) and reused: a `develop` dispatch sets `build_base: false` and pulls main's published base, so it never +overwrites the branch-agnostic `nx-base` tag. The product build reads both branches' registry buildcaches +(`buildcache-main`, `buildcache-develop`) and writes only its own branch's cache, only when pushing. + +### Versioning: compute once, thread everywhere + +NBGV runs once (in [`get-version-task.yml`](./.github/workflows/get-version-task.yml)), classifying from +`github.ref`, and its outputs (`SemVer2`, `GitCommitId`) thread to every consumer via `outputs:` / `needs:` / +`semver2` inputs. `build-docker-task` accepts the threaded `semver2` as the image `LABEL_VERSION` and never +re-runs NBGV - one classification feeds every product leg, so no second NBGV run can reclassify or collide a +tag. A build job may check out a specific commit to compile it (main pins to `GitCommitId`) but consumes the +threaded version. `main` (the public ref, `publicReleaseRefSpec = ^refs/heads/main$`) builds a clean +`X.Y.`; every other branch a prerelease `X.Y.-g`. *Keeps each image's embedded version +and the release tag in agreement.* + +NBGV classifies `publicReleaseRefSpec` from the `GITHUB_REF` environment variable. Because the publisher +builds the **trigger ref** (one branch per run), `GITHUB_REF` already equals the branch being versioned - a +schedule, a pin push, or a `main` dispatch classifies as public; a `develop` dispatch as prerelease - so no +`GITHUB_REF` override is needed (`IGNORE_GITHUB_REF` is only for matrix publishers that build a non-trigger +branch). The main-version backstop (D2.2) catches any misclassification. + +NBGV's version is the GitHub release tag on `main` and the `LABEL_VERSION` build arg baked into the images. +The Docker image **tags** themselves carry the Nx product version from `Make/Matrix.json`, distinct from the +NBGV build version. + +### Validate at entry + +A run that carries a cross-input invariant (`main` must not carry a prerelease suffix) asserts it once with +`::error::` before the release is published. The `github-release` job `needs:` the version job and runs the +backstop step first. + +### Fast CI feedback, head-resolved + +CI runs on push to every branch, so GitHub head-resolves the reusable `./...` workflows from the pushed head: +a pull request that edits a reusable task tests its own copy. CI validates (the reusable `validate-task`: +Husky lint + `dotnet test`) on every push, and smoke-builds a representative image subset only when image +files changed (an inline `git diff` change-gate, no `dorny/paths-filter`), uploading and pushing nothing. One +aggregator job, the ruleset-bound required check, gates the merge. A branch-deletion push (all-zeros +`github.sha`) is skipped by a `!github.event.deleted` guard on every job, so a deletion never runs a failing +build. + +### The Docker-only release + +The `github-release` job tags the built commit and creates the GitHub release (auto source zip + README + +LICENSE; `target_commitish` pinned to `GitCommitId`; skip-existing guard; main-only). This repo ships **no** +`release-asset-*` binaries or packages - the published artifacts are the Docker Hub images - so there is no +release-asset download step and `fail_on_unmatched_files` is omitted. The GitHub release exists only as the +version anchor / tag. The Docker Hub repository overview (the repo `README.md`) is pushed to every product + +base repo on a `main` Docker publish (the `docker-readme` jobs), since Docker Hub does not read the GitHub +README; the repo list is derived inline from `Make/Matrix.json`. + +### Resource lifecycle + +Workflow artifacts are an intra-run handoff; the durable copy lives on the GitHub release / Docker Hub. The +publisher and CI both run a terminal `cleanup-artifacts` job that deletes the run's transfer artifacts so +they do not accumulate against the small account-wide storage quota; it is `continue-on-error` so housekeeping +never reds the run. + +### Self-sufficiency: automatic updates + +Every Dependabot pull request, any ecosystem and any tier, auto-merges once the required checks pass, except +a **semver-major NuGet** bump, which waits for human review. Codegen opens a `codegen-main` -> `main` and a +`codegen-develop` -> `develop` PR daily; the merge-bot auto-merges each independently (`--delete-branch`). +A merged dependency bump does not itself publish; a merged matrix pin on `main` does (the pin-push trigger). +A person steps in only for a breaking change (a red check) or to dispatch a release. + +### Flow diagrams + +Four diagrams trace the architecture above: the pull-request gate, the self-publisher, the bot +automation, and the trigger chain that turns a daily codegen run into a published release. They depict +the same outcomes that the section 4 contract specifies, drawn from the workflow YAML; if a diagram and +a guarantee disagree, one of them is a defect. Triggers are blue, gates yellow, durable/published +outputs green, and stop/skip outcomes red. + +**Pull request (CI) - `test-pull-request.yml`.** Every push head-resolves the reusable tasks, runs the +validate gate, smoke-builds a representative image subset only when image files changed, and a single +aggregator produces the ruleset-bound required check (D1, D6). + +```mermaid +flowchart TD + T(["push: every branch
(or workflow_dispatch)"]):::trig + T --> D{"github.event.deleted?"}:::gate + D -- "yes: branch deletion" --> X(["all jobs + aggregator skip
no failed run, no pending check"]):::stop + D -- "no" --> CH["changes job
inline git diff change-gate
image? base?"] + D -- "no" --> V["validate job
(validate-task.yml)"] + subgraph VT ["validate-task.yml"] + VU["Husky lint (CSharpier,
dotnet format style)
+ dotnet test"] + end + V --> VT + CH --> SG{"image files changed?
(Docker/**, Make/Matrix.json, Make/Version.json)"}:::gate + SG -- "no" --> SS(["smoke-build skipped
(aggregator allows skip)"]):::stop + SG -- "yes" --> S["smoke-build job
build-docker-task.yml
smoke: true, push: false
NxMeta + NxMeta-LSIO, amd64"] + CH --> A + VT --> A + S --> A + SS --> A + A{"Check pull request workflow status job
changes AND validate AND smoke-build
succeeded or skipped?"}:::gate + A -- "yes" --> G(["required check passes
merge unblocked"]):::pub + A -- "no" --> R(["required check fails
merge blocked"]):::stop + classDef trig fill:#dbeafe,stroke:#2563eb,color:#1e3a8a + classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 + classDef pub fill:#dcfce7,stroke:#16a34a,color:#14532d + classDef stop fill:#fee2e2,stroke:#dc2626,color:#7f1d1d +``` + +**Publish - `publish-release.yml`.** A weekly schedule (main), a `Make/Matrix.json` pin push (main), or a +dispatch versions once with NBGV, builds the shared base (main only), validates, builds the 12-image +product matrix with the threaded SemVer2, then cuts the main-only GitHub release and refreshes the Docker +Hub overviews (D0, D2, D3, D4). + +```mermaid +flowchart TD + P1(["schedule: weekly Mon 02:00 UTC
(main only)"]):::trig --> GV + P2(["push: main
paths = Make/Matrix.json (codegen pin)"]):::trig --> GV + P3(["workflow_dispatch
(main or develop)"]):::trig --> GV + GV{"get-version job
ref_name in (main, develop)?"}:::gate + GV -- "feature branch" --> GVS(["all jobs skip
no publish"]):::stop + GV -- "yes" --> GVR["get-version job
(get-version-task.yml)
NBGV @master, runs once
SemVer2 + GitCommitId"] + GVR --> BB{"ref_name == main?"}:::gate + BB -- "develop dispatch" --> BBS(["build-base skipped
reuse main's nx-base"]):::stop + BB -- "main" --> BBJ["build-base job
(build-base-images-task.yml)
nx-base + nx-base-lsio
amd64 + arm64, branch ref (github.ref_name)"] + GVR --> VAL["validate job
(validate-task.yml)
main: pinned to GitCommitId"] + VAL --> BD + BBJ --> BD + BBS --> BD + BD["build-docker job
(build-docker-task.yml, build_base: false)
12-image matrix from Make/Matrix.json
amd64 + arm64, max-parallel 4
LABEL_VERSION = threaded SemVer2"] + BD --> DH[("Docker Hub
10 product repos (branch tags)
+ 2 shared base repos")]:::pub + BD --> RG{"ref_name == main?"}:::gate + RG -- "develop" --> RGS(["no GitHub release
(:develop images only)"]):::stop + RG -- "main" --> VPR{"github-release job
SemVer2 has no prerelease '-'?
(strip +buildmetadata)"}:::gate + VPR -- "prerelease suffix" --> VPRX(["fail ::error::
refuse to publish"]):::stop + VPR -- "clean" --> EX{"tag exists AND not dispatch?"}:::gate + EX -- "yes" --> EXS(["skip release create
(no-op republish)"]):::stop + EX -- "no" --> REL[("GitHub release
tag = SemVer2 at GitCommitId
prerelease: false, source zip + README + LICENSE")]:::pub + BD --> DRR["docker-readme-repos job
derive repo list from Matrix.json"] + DRR --> DRM["docker-readme job (matrix)
push README to each Docker Hub repo"] + DRM --> DRO[("Docker Hub overviews
10 product + 2 base repos")]:::pub + classDef trig fill:#dbeafe,stroke:#2563eb,color:#1e3a8a + classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 + classDef pub fill:#dcfce7,stroke:#16a34a,color:#14532d + classDef stop fill:#fee2e2,stroke:#dc2626,color:#7f1d1d +``` + +**Automation - codegen + Dependabot + merge-bot.** Daily codegen and Dependabot open in-repo bot PRs; the +merge-bot enables auto-merge (or disables it on a maintainer push); the required check gates each merge +(D8). + +```mermaid +flowchart TD + SCH(["schedule daily 04:00 UTC
(or workflow_dispatch)"]):::trig --> CG + subgraph CGT ["run-codegen-pull-request-task.yml (matrix: main, develop)"] + CG["codegen job per branch
regenerate Version.json + Matrix.json
(deterministic, forward-only guard)"] --> CGC{"data changed?"}:::gate + CGC -- "no" --> CGN(["no PR"]):::stop + CGC -- "yes" --> CPR["open codegen-<branch> PR
(App token)"] + end + DEP(["Dependabot opens PR
any ecosystem/tier"]):::trig --> MB + CPR --> MB + subgraph MBT ["merge-bot-pull-request.yml (pull_request_target)"] + MB{"event / author"}:::gate + MB -- "opened/reopened
bot author" --> EN["enable auto-merge --delete-branch
squash develop / merge main"] + MB -- "synchronize by maintainer" --> DIS["disable auto-merge"] + end + EN --> SM{"semver-major NuGet?"}:::gate + SM -- "yes" --> HUM(["wait for human review"]):::stop + SM -- "no" --> CK{"required check passes?"}:::gate + CK -- "yes" --> MRG(["PR merges (App token)"]):::pub + CK -- "no" --> BLK(["merge blocked
maintainer notified"]):::stop + MRG -. "codegen-main Matrix.json change" .-> PUBR(["publisher pin-push auto-publishes main"]):::pub + classDef trig fill:#dbeafe,stroke:#2563eb,color:#1e3a8a + classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 + classDef pub fill:#dcfce7,stroke:#16a34a,color:#14532d + classDef stop fill:#fee2e2,stroke:#dc2626,color:#7f1d1d +``` + +**Trigger chain - daily codegen to published release.** The recurring path that ships a new upstream Nx +product version with no human in the loop: only a `codegen-main` matrix change reaches the publisher's +pin push; the develop pin update is sync-only, and `:develop` / the weekly base refresh come from the +schedule and dispatch (D4.1, D8.3). + +```mermaid +flowchart TD + PER(["schedule daily 04:00 UTC
run-periodic-codegen-pull-request.yml
(or workflow_dispatch)"]):::trig --> CGM + CGM["codegen matrix: main + develop
regenerate Version.json + Matrix.json"] --> DCH{"Matrix.json changed?"}:::gate + DCH -- "no" --> NOPR(["no PR, nothing ships"]):::stop + DCH -- "yes: main" --> PRM["codegen-main -> main PR
(merge-bot auto-merges)"] + DCH -- "yes: develop" --> PRD["codegen-develop -> develop PR
(merge-bot auto-merges)"] + PRD --> SYNC(["develop Matrix.json updated
sync-only, push trigger is main-only
:develop refreshed by dispatch"]):::stop + PRM --> PUSH(["push to main
paths = Make/Matrix.json"]):::trig + PUSH --> PUB["publish-release.yml
pin-push: build base + 12-image matrix"] + PUB --> SINK[("Docker Hub product + base images
+ main GitHub release")]:::pub + SCHED(["weekly schedule (main)
base refresh for CVEs"]):::trig --> PUB + DISP(["workflow_dispatch (main or develop)
force publish that branch"]):::trig --> PUB + classDef trig fill:#dbeafe,stroke:#2563eb,color:#1e3a8a + classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 + classDef pub fill:#dcfce7,stroke:#16a34a,color:#14532d + classDef stop fill:#fee2e2,stroke:#dc2626,color:#7f1d1d +``` + +## 4. Behavioral contract - expected outcomes + +Each is a **MUST**, stated as input -> output plus the failure it prevents. + +### D0 - Architecture + +- **D0.1 CI is one run, one branch.** Input: any push. Output: `test-pull-request` builds/validates exactly + `github.ref_name` and publishes nothing. *Prevents cross-branch ref mixing in CI.* +- **D0.2 The publisher builds one branch: the trigger ref.** Output: the publisher passes `github.ref_name` + as `ref` and `branch`, so it checks out, versions, and tags exactly the run's own branch (the schedule/pin + push's `main`, or a dispatch's branch). No branch matrix; the jobs are guarded to `main`/`develop`. + *Prevents cross-branch ref mixing - `github.ref` is the branch being published.* +- **D0.3 One version, threaded.** Output: NBGV runs once (`get-version-task`); every consumer reads it via + `needs:` outputs / the `semver2` input; no consumer recomputes it. *Allowed:* checking out a specific + commit to compile it, and recording the built commit as the release `target_commitish`. *Prevents an + image's embedded version diverging from its tag, and a second NBGV run colliding image tags.* + +### D1 - CI fast feedback + +- **D1.1 Every push validates; image changes smoke-build.** Output: on any push the `validate` job (the + reusable `validate-task`) runs with no paths filter; `smoke-build` (NxMeta + NxMeta-LSIO, amd64, no push) + runs when the inline change-gate detects an image-file change (`Docker/**`, `Make/Matrix.json`, + `Make/Version.json`). *Prevents a reusable-workflow or Dockerfile break shipping untested.* +- **D1.2 Unit tests always run.** Output: `validate-task` runs `dotnet test` (the codegen tool + its tests). +- **D1.3 Lint enforces the editor checks in CI.** Output: `validate-task` runs Husky (`dotnet husky run`: + CSharpier + `dotnet format style --verify-no-changes`) - the same checks the editor and the pre-commit + hook run. Workflow YAML is lint-checked by `actionlint` (run from the editor / locally). +- **D1.4 Smoke never publishes and never uploads a release asset.** Output: a smoke build compiles the image + subset but makes no GitHub release and no Docker push (`push: false`, `smoke: true`). The Docker login runs + on every build including smoke (for higher pull/cache rate limits against the registry buildcache), so a + Dependabot-triggered push-CI smoke build needs the Docker Hub credentials in both secret stores; fork PRs do + not run this push-CI. +- **D1.5 One required aggregator gates merge.** Output: a single aggregator job must **succeed** (success or, + for the conditionally-skipped `smoke-build`, skipped; never failure/cancelled), `needs:` `changes`, + `validate`, and `smoke-build`, and treats a `changes` failure as blocking (so an image-changing PR cannot + merge on a `changes` failure mis-read as skip). Its name is ruleset-bound (D6.2) and must not be renamed. + +### D2 - Validation at entry + +- **D2.1 Validate the cross-input invariant before publishing.** Output: the `github-release` job asserts each + cross-input invariant with `::error::` (the main-version backstop, D2.2) before the release is created; + downstream steps `needs:` the version job. The publish run also re-runs `validate-task` (the `validate` job + gates `build-docker`), so a publish can never ship a tree that would fail the same lint + `dotnet test` gate + CI enforces on push - on the trigger branch CI already validated, re-checked at publish time. +- **D2.2 Main matches version classification.** Input: a real publish run for `main`. Output: the release + fails loudly if `main` carries a prerelease suffix. It strips `+buildmetadata` before testing for the + prerelease `-`. *Prevents a develop build published as the stable `latest`.* + +### D3 - Versioning and classification + +- **D3.1 NBGV runs once, threaded.** Output: NBGV runs once, classifying from the checked-out branch; no + consumer re-invokes it. The run builds the trigger ref, so `GITHUB_REF` already matches the branch being + versioned and NBGV classifies it correctly (no `IGNORE_GITHUB_REF`). The threaded `semver2` is the image + `LABEL_VERSION`. +- **D3.2 `main` = stable, others = prerelease.** Output: `main` -> `X.Y.Z`, any other branch -> + `X.Y.Z-g`. The release-version backstop names `main`; `publicReleaseRefSpec` is `^refs/heads/main$`. +- **D3.3 Version floor + git height.** Output: `version.json` sets the major.minor floor, NBGV appends the git + height as the patch, never bumped on a cadence. *(Who raises the floor and when is a human-process rule in + [`GOVERNANCE.md` "Release Model"](./GOVERNANCE.md#release-model).)* + +### D4 - Release / publish + +- **D4.1 Publish only on schedule, the matrix-pin push, or dispatch - never on an ordinary merge.** Output: + `publish-release` triggers are `schedule` (weekly), `workflow_dispatch`, and a `push` **branch-filtered to + `main` and path-filtered to `Make/Matrix.json`**. There is no other `push` trigger and no `PUBLISH_ON_MERGE` + variable. The jobs are guarded to `github.ref_name` in (`main`, `develop`), so a stray dispatch from a + feature branch is a no-op. *Prevents per-merge release churn while still shipping a new product pin at once.* +- **D4.2 A publish builds the one trigger branch in full.** Output: the run builds the shared base (main run + only) + the full product matrix and creates the GitHub release for `github.ref_name` - the schedule/pin push + rebuilds `main` (stable / `latest`); a dispatch publishes its own branch (`main` stable / `latest`, + `develop` prerelease / `:develop`, no GitHub release). *Prevents a half-published image set and cross-branch + ref mixing.* +- **D4.3 Tag the built commit.** Output: the release `target_commitish` is the run's `GitCommitId` (the tip of + `main`), and main's images are built from that pinned commit, never the moving ref. *Prevents the tag / + embedded version landing on the wrong commit.* +- **D4.4 Release contents and gate.** Output: a release is a tag on the built commit plus the auto source zip, + README, and LICENSE - no binary release assets (`fail_on_unmatched_files` omitted; the images are the + artifact). The release is `main`-only (`github.ref_name == 'main'`); `prerelease: false`. *Prevents + publishing a develop build as a stable GitHub release.* +- **D4.5 No-op republish.** Input: a weekly re-run whose version is unchanged. Output: the release-create step + is skipped when the tag already exists (refreshed only on `workflow_dispatch`), while the Docker push still + runs - re-pushing the same tags refreshes the shared base image. *Prevents duplicate releases while still + refreshing the images.* +- **D4.6 Publish is built from the tree CI validated.** Output: the run's ref **is** the published branch, so + the reusable-task definitions and the built tree resolve from that branch - the same tree CI validated on + push (the required check gates every merge to it) with the identical `validate-task` definition - and the publish run re-runs that `validate-task` (the + `validate` job gates `build-docker`). The main-version backstop (D2.2) is the additional in-publisher gate. +- **D4.7 Docker publishing authenticates with Docker Hub credentials.** Output: the base and product builds + log in via `docker/login-action` with `DOCKER_HUB_USERNAME` + `DOCKER_HUB_ACCESS_TOKEN` and push with + `docker/build-push-action`; the Docker Hub overview is pushed with the same token. There is **no NuGet/OIDC + publishing** in this repo. *Prevents a missing-credential publish failure.* +- **D4.8 Branch-scoped Docker buildcache.** Output: the base and product builds read both branches' registry + caches (`buildcache-main`, `buildcache-develop`) and write only their own branch's cache, only when pushing, + so a `main` and a `develop` publish never overwrite each other's cache. *Prevents one branch's publish + destroying the other's cache hit-rate.* +- **D4.9 Multi-arch, multi-product, shared-base fan-out.** Output: the publish builds every product image from + `Make/Matrix.json` for `linux/amd64` + `linux/arm64`, on the shared base built once and reused; each product + image pushes to its own Docker Hub repo. *Prevents a per-arch or per-product gap, and a redundant per-branch + base rebuild overwriting the shared tag.* + +### D5 - Resource cleanup + +- **D5.1 Terminal cleanup, best-effort.** The publisher and CI each run a terminal `cleanup-artifacts` job + (`always()`, `continue-on-error`) that deletes the run's transfer artifacts, independent of the required + aggregator so housekeeping never gates the merge. +- **D5.2 Never red the run on cleanup.** Cleanup failures warn, never fail. + +### D6 - Self-testing workflows + +- **D6.1 A change is testable on its own branch.** Output: a workflow or build change is exercised by CI on + the branch that introduces it, no dependency on reaching `main` first. +- **D6.2 Head-resolution, single producer, fork exception.** Output: CI runs on `push` to every branch so + reusable `./...` logic resolves from the head, and the aggregator's ruleset-bound `context:` is produced by + that push run as the sole producer of that name. Dependabot and codegen PRs are in-repo branches, validated + the same way. A fork cannot push, so it has no run and is validated by maintainer action - the one + exception. *Prevents a dual-producer context race and a false self-test claim for forks.* + +### D7 - Concurrency, permissions, safety + +- **D7.1 The publisher does not cancel mid-flight.** Output: ref-independent group, `cancel-in-progress: + false`. CI uses the `...-${{ github.ref }}` group with `cancel-in-progress: true`; the merge-bot keys on PR + number (D8.1); codegen keys on the workflow. +- **D7.2 Skipped jobs still need valid permissions.** Output: every reusable job runs under valid least-privilege + `permissions:`; a callee's extra scope is granted by the caller. +- **D7.3 Boolean inputs both forms.** Declared in both trigger blocks, compared against `true` and `'true'`. +- **D7.4 Branch-deletion guard.** Output: every CI job carries `if: !github.event.deleted` and the aggregator + `always() && !github.event.deleted`, so a branch-deletion push (all-zeros `github.sha`) runs no failing + build and does not fail the required check. + +### D8 - Bots and automation + +- **D8.1 Merge-bot.** Output: runs on `pull_request_target`, holds the App token, merges the PR by URL without + checking out its code. Enables auto-merge on `opened`/`reopened` with `--delete-branch`; squash on + `develop`, merge-commit on `main` by the PR's base ref; disables auto-merge when a maintainer pushes to a + bot branch (no `--delete-branch` on the disable path). Concurrency keyed on PR number. +- **D8.2 Dependabot auto-merges on green, semver-major NuGet excepted.** Output: every Dependabot PR + auto-merges once the required checks pass, except a semver-major NuGet bump (human review). A failing check + blocks the merge. A merged dependency bump does **not** itself publish (it ships in the next scheduled run + or the next matrix-pin push). +- **D8.3 Codegen dual-targets `main` AND `develop`.** Output: the daily codegen matrix opens a + `codegen-main` -> `main` and a `codegen-develop` -> `develop` PR (strict head/base pairing in the merge-bot), + regenerating `Make/Version.json` + `Make/Matrix.json` **only** (not the Dockerfiles - those are a separate + human-driven `Make/Create.sh` path). The merge-bot auto-merges each independently. develop's matrix update + is sync-only; a merged `codegen-main` matrix change fires the publisher's pin push. *Prevents non-linear + rebase/merge-block conflicts (the rejected single-target scheme) and keeps both branches current with + upstream Nx versions.* +- **D8.4 Forward-only version guard.** Output: the codegen tool's forward-only guard (`ReleaseVersionForward`, + covered by `CreateMatrixTests`) prevents a regenerated matrix from regressing a product version. *Prevents a + generic-tag downgrade slipping in via codegen.* + +### D9 - Style, static, and dropped workflows (see section 2) + +- **D9.1** Every action SHA-pinned with a version comment (sole exception: `dotnet/nbgv@master`, whose tag + stream lags master so Dependabot tag-tracking would only propose downgrades to stale tags - a deliberate + documented float, rationale inline in `get-version-task.yml`); an installed-tool version is left unpinned to + track latest. +- **D9.2** File/workflow/job/step names follow the suffix rules; a ruleset-bound `context:` name moves only in + lockstep with the live ruleset and the hub payload it is applied from. +- **D9.3** Bash `run:` blocks start `set -Eeuo pipefail`; multi-line `if:` uses `>-`. +- **D9.4** Line endings follow `.editorconfig`. +- **D9.5 No decorative / dropped workflows.** No date-badge (`build-datebadge-*`), no standalone docker-readme + task (folded into the publisher), no `PUBLISH_ON_MERGE` variable, no `dorny/paths-filter` (replaced by the + inline change-gate). Their presence is a defect to remove. +- **D9.6** Style is enforced in CI by `validate-task` (D1.3), from the same config files the editor and Husky + hook use. + +### D10 - Repository configuration + +- **D10.1 Required configuration is present.** Output: the secrets, branch rulesets, and repository settings + section 6 lists are all in place, codified in the hub's repository-configuration payloads and audited by the + hub's `configure.sh check` (the 5D audit). The detail is in section 6. + +## 5. Test methodology + +### 5A. Static audit (no execution) + +Read the workflow files plus `version.json` and `Make/Matrix.json` and assert the fact behind each applicable +guarantee with a `file:line` citation: + +- **D0:** CI has no branch matrix; the publisher passes `github.ref_name` as `ref`/`branch` and is guarded to + `main`/`develop`; NBGV invoked once in `get-version-task`, `build-docker-task` has no nested `get-version` + and consumes the `semver2` input; the run builds the trigger ref so `GITHUB_REF` matches the versioned + branch. +- **D1:** CI runs on `push` with no paths filter on `validate`; the inline `changes` gate sets `image`/`base`; + `smoke-build` is `smoke: true`, `push: false`, amd64 NxMeta subset; the aggregator `needs:` `changes` + + `validate` + `smoke-build`, blocks on non-success, treats `changes` failure as blocking. +- **D2:** the main release backstop checks the prerelease `-`, strips `+buildmetadata`; `validate-task` is the + shared gate. +- **D3:** `main` appears in the backstop and the release `prerelease: false`/guard; `publicReleaseRefSpec` is + `^refs/heads/main$`; `semver2` threads to `LABEL_VERSION`. +- **D4:** `publish-release` triggers are `schedule` + `workflow_dispatch` + `push` (branches `[main]`, paths + `[Make/Matrix.json]`) only (no other push, no `PUBLISH_ON_MERGE`); jobs guarded to `github.ref_name` in + (`main`, `develop`); `target_commitish` = `GitCommitId`; main pins `ref` to `GitCommitId`, develop reuses + the base (`build_base: false`); the build logs in with `DOCKER_HUB_*`; buildcache branch-scoped and + write-gated on push; release-create gated `exists == false || workflow_dispatch`; release + docker-readme + gated to `main`; the product matrix builds amd64+arm64 from `Make/Matrix.json`. +- **D5:** the publisher and CI each have a terminal `cleanup-artifacts` (`always()`, `continue-on-error`), + independent of the aggregator. +- **D6:** CI is `push` on every branch; the aggregator context has exactly one producer; no `pull_request` + trigger; every CI job has the `!github.event.deleted` guard. +- **D7:** the publisher group is ref-independent with `cancel-in-progress: false`; the merge-bot keys on PR + number; codegen keys on the workflow; CI uses the standard group + deletion guard; reusable jobs declare + permissions. +- **D8/D9:** the merge-bot runs on `pull_request_target` with the App token, keyed on PR number, both merge + jobs use `--delete-branch`; Dependabot auto-merge excepts semver-major NuGet only; codegen dual-targets + main+develop and regenerates only `Version.json`/`Matrix.json`; no date-badge, standalone docker-readme + task, `PUBLISH_ON_MERGE`, or `dorny/paths-filter`; actions SHA-pinned except `nbgv@master`; + names/shells/conditionals per section 2. + +### 5B. End-to-end trace scenarios (deterministic from the YAML) + +| # | Input | Expected output | Exercises | +| --- | --- | --- | --- | +| S1 | push touching `Docker/**` | `validate` + `smoke-build` (NxMeta amd64) run, **no push, no release**; aggregator success; no dangling artifacts | D0.1, D1 | +| S2 | push changing only docs | `validate` runs; the `changes` gate sets `image=false`; `smoke-build` skipped; aggregator success (skip allowed) | D1, D1.5 | +| S3 | push changing only `.github/workflows/**` | `validate` runs head-resolved; `smoke-build` skipped (no image files); aggregator success | D1.1, D6.1 | +| S4 | weekly `schedule` | builds + publishes `main` only: shared base refresh + full product matrix (amd64+arm64) + stable release + `latest`; `target_commitish` = main's SHA; develop untouched; no dangling artifacts | D4.1, D4.2, D4.9 | +| S5 | push to `main` changing `Make/Matrix.json` (codegen pin) | publishes `main` with the new product versions immediately | D4.1, D8.3 | +| S6 | `workflow_dispatch` from `develop` | builds + publishes `develop`: `:develop` images, prerelease classification, `build_base: false` (reuses main's base), **no GitHub release** | D4.1, D4.2, D3.2 | +| S7 | `workflow_dispatch` re-run on `main`, no new commits | release-create refreshed on dispatch (skipped on schedule if the tag exists); Docker re-pushed (base refresh); no duplicate release | D4.5 | +| S8 | `workflow_dispatch` from a feature branch | the `github.ref_name in (main, develop)` guard skips every job -> no publish | D4.1 | +| S9 | merged dependency bump (any) | not a matrix-pin change; merges don't publish -> **no release**; ships in the next scheduled run | D4.1, D8.2 | +| S10 | merged develop codegen PR (`Matrix.json` change on develop) | sync-only; the pin push is main-only -> **no publish** | D8.3 | +| S11 | PR with a CSharpier / format / unit-test failure | `validate` fails -> aggregator blocks the merge | D1.2, D1.3, D1.5 | +| S12 | `version.json` floor bump merged | merges don't publish -> no immediate release; the new floor ships in the next publish | D3.3, D4.1 | +| S13 | Dependabot semver-major NuGet bump | gated on human review -> does not auto-merge; other majors auto-merge on green | D8.2 | +| S14 | branch-deletion push | every CI job + the aggregator skip (`!github.event.deleted`) -> no failing required check | D7.4 | +| S15 | `develop` -> `main` promotion (merge commit) | the merge itself does not publish; if it changed `Matrix.json` the pin push publishes main, else the next schedule does | D4.1, D8.1 | + +### 5C. Live probe (where warranted, never publishing) + +- Open a trivial doc-only PR and confirm S2 (`validate` runs, `smoke-build` skipped, aggregator green, 0 + artifacts left); open an image-touching PR and confirm S1 (smoke-build runs amd64, nothing pushed). +- After a `main` publish (schedule, pin push, or dispatch) confirm a stable release (`isPrerelease == false`) + with multi-arch product images (`docker buildx imagetools inspect` shows amd64 + arm64) and the shared base + tags intact; after a `develop` dispatch confirm `:develop` images, prerelease classification, and that the + shared `nx-base:ubuntu-noble` tag was not overwritten. A re-run adds no duplicate release. Absent publish + rights, record indeterminate and rely on 5A/5B. +- Trigger codegen via dispatch: confirm both `codegen-main` -> `main` and `codegen-develop` -> `develop` PRs + open and the merge-bot auto-merges with branch deletion. + +### 5D. Configuration audit + +Run the hub's `repo-config/configure.sh check / release` from a hub checkout at `main`. It +confirms the listed secrets exist (in both +stores), the `main`/`develop` rulesets enforce the required merge method + status check + signed commits + +strict-off, and the repository settings are in place, exiting non-zero on drift. Secret *values* cannot be +read back, so it asserts the names exist (failing if it cannot query them). The GitHub App installation is a +best-effort check (a precise check needs app-level auth, so it notes rather than fails). The Docker Hub +token's validity and push scope across the 12 repositories are a manual checklist item. + +### Assessment + +Operational when every applicable 5A item passes, every applicable 5B scenario matches (corroborated by 5C +where a live signal exists), and 5D configuration is in place. Procedure: **Audit** (5A + 5D) -> **Trace** +(5B) -> **Probe** (5C, without publishing) -> **Verdict** with the failing guarantee(s) and the triggering +input for each. + +## 6. Repository configuration + +The workflows depend on configuration outside the YAML. A misconfiguration surfaces only as a failed run, so +the configuration is part of "operational" (D10; audit 5D). + +**Secrets.** + +- `DOCKER_HUB_USERNAME` / `DOCKER_HUB_ACCESS_TOKEN` - Docker Hub credentials the base and product builds log + in with to push the images and the repository overviews. Required in **both** the Actions and Dependabot + secret stores: a Dependabot-triggered push runs CI whose Docker smoke build logs in too (when an image file + changed), and that run gets the Dependabot store. The access token needs push scope on every product + + base repo under `docker.io/ptr727/` (derived from `Make/Matrix.json`). There + is no NuGet/OIDC publishing. +- `CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY` - the GitHub App credentials the merge-bot and the + codegen PR-opener mint the App token from. Required in **both** the Actions and Dependabot secret stores (a + Dependabot-triggered run gets the Dependabot store). The App must be installed on the repo with + `contents: write` and `pull_requests: write`. +- The built-in `GITHUB_TOKEN` needs no setup. **No `PUBLISH_ON_MERGE` variable is used.** + +**Branch rulesets.** + +- `main` - merge-commit merges only; requires the aggregator status check (`Check pull request workflow status + job`); requires signed commits; "require branches up to date before merging" is **off** (a forward-only + `develop` makes every post-release `main` tip unreachable from `develop`, so the strict check would fail + every release). +- `develop` - squash merges only (keeps history linear); requires the same status check; requires signed + commits; "up to date" is **off** (so same-batch bot PRs auto-merge in parallel). +- The required check's `context:` matches the aggregator job name verbatim (D6.2, D9.2). + +**Repository settings.** Auto-merge enabled; squash and merge-commit both allowed (each ruleset narrows its +branch to one); rebase off; auto-delete-on-merge **off** (so `main`/`develop` survive a promotion; the +merge-bot deletes bot branches explicitly with `--delete-branch`). Dependabot version **and** security updates +enabled. The GitHub App installed with the scopes above. + +**Validation.** This configuration is codified in the hub's repository-configuration payloads and +applied/audited by the hub's `configure.sh`; `check` is the 5D audit. Secret values cannot be read back, so the audit asserts +the names exist (failing if they cannot be queried); the App installation is a best-effort check. diff --git a/codecov.yml b/codecov.yml index 896c3789..7200a8c9 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,16 +1,16 @@ -# Codecov configuration. Coverage is reported and trended, never gated. -# - informational: true makes project and patch statuses advisory (always pass), so a coverage delta never blocks a PR. -# - Distinct from the upload step's fail_ci_if_error: false, which only guards upload errors. -# - A repo may override this to enforce a coverage threshold. -coverage: - status: - project: - default: - informational: true - patch: - default: - informational: true - -# Exclude code intentionally not unit-tested from the coverage denominator. Add the repo's own paths. -# ignore: -# - "Sandbox/**" +# Codecov configuration. Coverage is reported and trended, never gated. +# - informational: true makes project and patch statuses advisory (always pass), so a coverage delta never blocks a PR. +# - Distinct from the upload step's fail_ci_if_error: false, which only guards upload errors. +# - A repo may override this to enforce a coverage threshold. +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true + +# Exclude code intentionally not unit-tested from the coverage denominator. Add the repo's own paths. +# ignore: +# - "Sandbox/**" diff --git a/cspell.json b/cspell.json index e2c505b9..2a4b00f8 100644 --- a/cspell.json +++ b/cspell.json @@ -1,178 +1,244 @@ -{ - "version": "0.2", - "language": "en-US", - "ignorePaths": [ - ".git/**", - "reports/*/**", - "**/*.min.*" - ], - "ignoreRegExpList": [ - "/```mermaid[\\s\\S]*?```/g" - ], - "words": [ - "accessibilities", - "Allman", - "apikey", - "arahata", - "astral", - "autoremove", - "btrfs", - "buildcache", - "buildcaches", - "buildmetadata", - "buildtransitive", - "Buildx", - "charliermarsh", - "chowned", - "chowns", - "codegen", - "commitish", - "conftest", - "contentfiles", - "cref", - "crefs", - "csdevkit", - "datebadge", - "davidanson", - "debuglevel", - "devcontainer", - "diffable", - "digitalwatchdog", - "distros", - "dockerbuild", - "dockerdirectory", - "Dockerfiles", - "dockerhub", - "Docstrings", - "dogfoods", - "dorny", - "dotnettools", - "downstreams", - "dryrun", - "dwspectrum", - "Emby", - "envsubst", - "extensionless", - "fanaticpythoner", - "finalizers", - "Genericize", - "gpgsign", - "gruntfuggly", - "HACS", - "Hanwha", - "hatchling", - "hddpool", - "heredocs", - "homeassistant", - "idempotent", - "idempotently", - "imagetools", - "IPVMS", - "isort", - "Jellyfin", - "Keychain", - "kicad", - "kinnairdclan", - "lastbuild", - "libsecret", - "LINQ", - "logfile", - "lsio", - "lsiobase", - "macvlan", - "makedirectory", - "matrixpath", - "mediaserver", - "metavms", - "mktemp", - "Moby", - "mypy", - "nameof", - "nbgv", - "nektos", - "Nerdbank", - "networkoptix", - "Newtonsoft", - "noatime", - "nodev", - "noninteractive", - "nonroot", - "noqa", - "nosuid", - "nuget", - "nugetlibrary", - "nvrpool", - "nxgo", - "nxmeta", - "nxvms", - "nxwitness", - "onCreateCommand", - "Optix", - "othercommand", - "paramref", - "PGID", - "Pieter", - "pipefail", - "posixacl", - "postCreateCommand", - "prerelease", - "ProjectTemplate", - "PUID", - "purpleair", - "pydantic", - "pypa", - "pypi", - "pypilibrary", - "pyproject", - "pyright", - "pyrightconfig", - "pytest", - "quoteoftheday", - "reimplementation", - "relatime", - "Repot", - "resharper", - "rhysd", - "Rubba", - "ruff", - "sdist", - "seccomp", - "semver", - "Serilog", - "settingsfile", - "shellcheck", - "shfs", - "signingkey", - "slnx", - "snupkg", - "softprops", - "somecommand", - "ssdpool", - "sshconfig", - "subsetting", - "subvol", - "subvolid", - "timonwong", - "tmpfs", - "Triaging", - "trunc", - "tzdata", - "unbuilt", - "Unraid", - "unvalidated", - "updateversion", - "USERPROFILE", - "venv", - "versionlabel", - "versionpath", - "Viljoen", - "whitelabel", - "winget", - "Wisenet", - "wisenetwave", - "xattr", - "Xsession", - "xunit", - "yzhang" - ] -} +{ + "version": "0.2", + "language": "en-US", + "ignorePaths": [ + ".git/**", + "reports/*/**", + "**/*.min.*" + ], + "ignoreRegExpList": [ + "/```mermaid[\\s\\S]*?```/g" + ], + "words": [ + "accessibilities", + "adduser", + "Allman", + "apikey", + "appdata", + "arahata", + "astral", + "autoremove", + "backgrounded", + "backgrounding", + "btrfs", + "buildcache", + "buildcaches", + "buildmetadata", + "BUILDPLATFORM", + "buildtransitive", + "Buildx", + "buildx", + "bumpable", + "charliermarsh", + "chowned", + "chowns", + "cloudhost", + "codegen", + "codepoint", + "commitish", + "Composefile", + "conftest", + "connrefused", + "contentfiles", + "CRCRLF", + "cref", + "crefs", + "csdevkit", + "datebadge", + "davidanson", + "debuglevel", + "desync", + "devcontainer", + "diffable", + "digitalwatchdog", + "distros", + "dnsmasq", + "dockerbuild", + "dockerdirectory", + "dockerenv", + "Dockerfiles", + "dockerhub", + "Docstrings", + "dogfoods", + "domainname", + "dorny", + "dotnettools", + "downstreams", + "dpkg", + "dryrun", + "dwspectrum", + "Eeuo", + "Emby", + "envsubst", + "esac", + "esphome", + "extensionless", + "fanaticpythoner", + "filesystems", + "finalizers", + "Genericize", + "getent", + "getmatrix", + "gpgsign", + "gpgv", + "gruntfuggly", + "HACS", + "Hanwha", + "hass", + "hatchling", + "hddpool", + "heredocs", + "homeassistant", + "idempotent", + "idempotently", + "imagetools", + "IPVMS", + "isort", + "isprintable", + "Jellyfin", + "keepends", + "Keychain", + "keyring", + "keyrings", + "kicad", + "kinnairdclan", + "koalaman", + "lastbuild", + "libsecret", + "LINQ", + "logfile", + "lsio", + "lsiobase", + "macvlan", + "makedirectory", + "matrixpath", + "matrixschemapath", + "mediaserver", + "metavms", + "mktemp", + "Moby", + "mstruebing", + "mypy", + "nameof", + "nbgv", + "nektos", + "Nerdbank", + "networkoptix", + "Newtonsoft", + "noatime", + "nodev", + "noninteractive", + "nonroot", + "NONROOT", + "NOPASSWD", + "noqa", + "nosuid", + "nuget", + "nugetlibrary", + "nvrpool", + "nxgo", + "nxmeta", + "NXMETA", + "nxvms", + "nxwitness", + "NXWITNESS", + "onCreateCommand", + "opencode", + "opencontainers", + "Optix", + "othercommand", + "paramref", + "PGID", + "Pieter", + "pipefail", + "posixacl", + "postCreateCommand", + "prerelease", + "ProjectTemplate", + "PRRT", + "PSScriptAnalyzer", + "PUID", + "purpleair", + "pydantic", + "pypa", + "pypi", + "pypilibrary", + "pyproject", + "pyright", + "pyrightconfig", + "pytest", + "Qodo", + "quoteoftheday", + "regen", + "reimplementation", + "relatime", + "repoint", + "repointed", + "Repot", + "resharper", + "resync", + "resynced", + "rhysd", + "Rubba", + "ruff", + "schemamatrix", + "schemaversion", + "sdist", + "seccomp", + "semver", + "Serilog", + "settingsfile", + "shellcheck", + "shfmt", + "shfs", + "signingkey", + "Signup", + "slnx", + "snupkg", + "softprops", + "somecommand", + "splitlines", + "ssdpool", + "sshconfig", + "stdlib", + "subsetting", + "subshells", + "subvol", + "subvolid", + "tagsargs", + "TARGETARCH", + "TARGETPLATFORM", + "timonwong", + "tmpfs", + "traefik", + "Triaging", + "trunc", + "tzdata", + "unbuilt", + "unconfigured", + "unperformable", + "Unraid", + "unreviewed", + "untriaged", + "unvalidated", + "updateversion", + "usermod", + "USERPROFILE", + "uvx", + "venv", + "versionlabel", + "versionpath", + "versionschemapath", + "Viljoen", + "whitelabel", + "winget", + "Wisenet", + "wisenetwave", + "WORKDIR", + "worktree", + "xattr", + "Xsession", + "xunit", + "yzhang" + ] +} diff --git a/global.json b/global.json new file mode 100644 index 00000000..3140116d --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/host-tools.json b/host-tools.json new file mode 100644 index 00000000..48d9d871 --- /dev/null +++ b/host-tools.json @@ -0,0 +1,4 @@ +{ + "note": "This repository's own host-tool declaration, layered over the fleet declaration in the hub's spec/host-tools.json by scripts/host_gate.py. Layering is tighten-only: an entry here may add a tool, raise a floor, or turn an optional tool required, and may not lower a floor or turn a required tool optional, because that would retire a fleet check from inside the repository it protects. The tools list is empty because this repository needs no tool the fleet declaration does not already carry, and the file is still present rather than absent so the declaration is somewhere a reader can find rather than somewhere they have to know to look.", + "tools": [] +} diff --git a/repo-config/README.md b/repo-config/README.md deleted file mode 100644 index 5cd59cc6..00000000 --- a/repo-config/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# repo-config - -Repository configuration as code - the parts of "operational" that live in GitHub settings rather than -in workflow YAML: branch rulesets, repository settings, and the secrets the workflows read. This is the -concrete form of [`WORKFLOW.md`](../WORKFLOW.md) section 6 and guarantee **D10**, and the implementation -of its **5D configuration audit**. - -This directory is intentionally **not** under `.github/` - that path is GitHub's own (workflows, issue -templates); repository administration config-as-code is the maintainer's, so it lives here. - -## Files - -- [`configure.sh`](./configure.sh) - idempotent `gh api` script with two modes: - - `./repo-config/configure.sh check` - validate only, no writes; exits non-zero on drift (the 5D - audit). Read-only, but it reads the rulesets and secrets endpoints, so it still needs a `gh` token - with admin on the repo. - - `./repo-config/configure.sh apply` - create-or-update the rulesets and settings to match this - directory (needs admin; writes). -- [`ruleset-develop.json`](./ruleset-develop.json) - the `develop` branch ruleset (squash-only, linear - history, signed commits, the required status check, strict-status **off**). -- [`ruleset-main.json`](./ruleset-main.json) - the `main` branch ruleset (merge-commit-only, signed - commits, the same required check, strict **off**; no linear-history rule). -- [`settings.json`](./settings.json) - repository settings (auto-merge on; squash **and** merge-commit - allowed; rebase off; auto-delete-on-merge **off**). The repo-wide auto-delete **setting** is off so a - `develop -> main` promotion does not delete `develop` (GitHub's auto-delete would remove the merged head - branch). Per-merge deletion is explicit instead: the merge-bot deletes a merged bot branch with - `gh pr merge --delete-branch`, and a feature branch is deleted the same way (or via the merge UI's delete - button) - so `main`/`develop` survive while bot/feature branches are still cleaned up. - -## What it does not store - -Secret **values** are never readable through the API, so the script only asserts the required secret -**names** exist (`DOCKER_HUB_USERNAME` / `DOCKER_HUB_ACCESS_TOKEN` for the images, and the App credentials -`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY` for the merge-bot and the codegen pull-request -automation), and *notes* (best-effort) whether a GitHub App is installed - a precise check needs app-level -auth, so the App-installation check does not fail the audit. The Docker Hub and App credentials must be set -in **both** the Actions and Dependabot secret stores, since a Dependabot-triggered run gets the Dependabot -store. Set the values in the repository (or organization) secret store directly. There is no NuGet -publishing here (the .NET project is a build-time matrix generator only); the GitHub release uses the -built-in `GITHUB_TOKEN`. The Docker Hub access token's validity and push scope are verified by hand, not by -this script. - -The Docker Hub repositories this token must push to are derived from -[`Make/Matrix.json`](../Make/Matrix.json): one `docker.io/ptr727/` per product image plus the shared -bases `nx-base` and `nx-base-lsio`. As of writing that is `dwspectrum`, `dwspectrum-lsio`, `nxgo`, -`nxgo-lsio`, `nxmeta`, `nxmeta-lsio`, `nxwitness`, `nxwitness-lsio`, `wisenetwave`, `wisenetwave-lsio`, -`nx-base`, and `nx-base-lsio` (12 repositories). - -## Applying, and the required-check rename lockstep - -The live ruleset's required status check is matched by **name** to the aggregator job in -[`test-pull-request.yml`](../.github/workflows/test-pull-request.yml) (`Check pull request workflow -status job`). GitHub binds the check by that exact string, so the ruleset JSON here, the live ruleset, and -the aggregator job name must move **in lockstep** ([`WORKFLOW.md`](../WORKFLOW.md) D6.2). If they drift, a -pull request runs CI but its required check never resolves and the PR cannot merge. - -So whenever the ruleset JSON or that job name changes, run `apply` against the live repo in the same -change that ships the workflow edit, then `check`: - -```sh -REPO=ptr727/NxWitness ./repo-config/configure.sh apply # sync live rulesets + settings + security -REPO=ptr727/NxWitness ./repo-config/configure.sh check # confirm no drift -``` - -First-time adoption is the same step: the live ruleset predates the renamed aggregator, so the first -`apply` is what lets a pull request against the new workflows go green. Both modes need a `gh` login -with admin on the repo (the rulesets and secrets endpoints require it). `apply` writes, `check` only -reads. - -## Why both a script and JSON - -The JSON files are the unambiguous source of truth for the configuration; the script applies and audits -them idempotently. An agent can also derive the same checks on the fly from `WORKFLOW.md` section 6, but -the committed script and JSON codify the exact intended state so the configuration is reproducible and -diffable rather than tribal knowledge. diff --git a/repo-config/configure.sh b/repo-config/configure.sh deleted file mode 100755 index ce12dcbc..00000000 --- a/repo-config/configure.sh +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env bash -# Repository configuration as code - the secrets, branch rulesets, and settings the workflows assume -# (see WORKFLOW.md section 6, guarantee D10). Idempotent: `apply` configures a repo to match the JSON in -# this directory; `check` validates an existing repo and exits non-zero on drift (the 5D audit). Run from -# anywhere; the target repo is resolved from the current `gh` context unless $REPO is set (owner/name). -# -# ./repo-config/configure.sh check # validate only, no writes (the 5D audit) -# ./repo-config/configure.sh apply # create-or-update rulesets + settings (writes) -# -# Requires gh and jq. Both modes read the rulesets and secrets endpoints, which need admin on the repo, so -# gh must be authenticated with admin for `check` as well as `apply`. `check` only reads; `apply` writes. - -set -euo pipefail - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO="${REPO:-$(gh repo view --json nameWithOwner --jq .nameWithOwner)}" - -# Secrets by store (names only; values are never readable via the API). The Docker Hub credentials and the -# merge-bot App credentials must be set in BOTH stores: a Dependabot-triggered run gets the Dependabot secret -# store, not Actions secrets, and that run's push CI builds the Docker smoke, which logs in to Docker Hub. -# Publishing the GitHub release uses the built-in GITHUB_TOKEN (no secret needed). -REQUIRED_ACTIONS_SECRETS=(DOCKER_HUB_USERNAME DOCKER_HUB_ACCESS_TOKEN CODEGEN_APP_CLIENT_ID CODEGEN_APP_PRIVATE_KEY) -REQUIRED_DEPENDABOT_SECRETS=(DOCKER_HUB_USERNAME DOCKER_HUB_ACCESS_TOKEN CODEGEN_APP_CLIENT_ID CODEGEN_APP_PRIVATE_KEY) -REQUIRED_CHECK="Check pull request workflow status job" - -note() { printf ' %s\n' "$*"; } -pass() { printf ' \033[32mok\033[0m %s\n' "$*"; } -fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; FAILED=1; } -FAILED=0 - -ruleset_id() { # name -> id (empty if absent); aborts with a visible reason on an API error - local out - # An absent ruleset is a successful call with no match (empty); only a real API error fails. Let gh print its - # own error on stderr (do not suppress it); add a generic context line and return non-zero so the run stops - # (the caller's $(...) cannot print the cause itself). - # per_page=100 returns every ruleset in one array (a repo has only a handful); the default page size is 30. - if ! out="$(gh api "repos/$REPO/rulesets?per_page=100")"; then - echo "ERROR: could not list rulesets for $REPO (see gh error above)" >&2 - return 1 - fi - # shellcheck disable=SC2016 # $n is a jq variable (--arg n), not a shell expansion - # Select the first match inside jq (not `| head -1`): under pipefail, head closing the pipe early can - # SIGPIPE jq and fail the function. - jq -r --arg n "$1" '[.[] | select(.name==$n) | .id] | first // empty' <<<"$out" -} - -apply_ruleset() { - local file="$1" name id - name="$(jq -r .name "$file")" - id="$(ruleset_id "$name")" - if [[ -n "$id" ]]; then - gh api -X PUT "repos/$REPO/rulesets/$id" --input "$file" >/dev/null - note "updated ruleset '$name' (#$id)" - else - gh api -X POST "repos/$REPO/rulesets" --input "$file" >/dev/null - note "created ruleset '$name'" - fi -} - -cmd_apply() { - echo "Applying repository configuration to $REPO" - apply_ruleset "$DIR/ruleset-develop.json" - apply_ruleset "$DIR/ruleset-main.json" - gh api -X PATCH "repos/$REPO" --input "$DIR/settings.json" >/dev/null - note "patched repository settings" - gh api -X PUT "repos/$REPO/vulnerability-alerts" >/dev/null - gh api -X PUT "repos/$REPO/automated-security-fixes" >/dev/null - note "enabled Dependabot alerts + security updates" - echo "Done. Run '$0 check' to validate." -} - -# --- validation (5D) ------------------------------------------------------------------------------- - -# assert MESSAGE TEST... - run the test command; pass on success, fail on non-zero (proper if/else, not -# the A && B || C footgun). The test command may read stdin (e.g. a `<<<` heredoc on the assert call). -# Do not redirect the assert call's stdout - that would also swallow the pass/fail line; commands that -# print (jq) use `jq_has`, which silences only itself. -assert() { - local msg="$1"; shift - if "$@"; then pass "$msg"; else fail "$msg"; fi -} - -# jq_has FILTER... - true iff the jq filter selects something; jq's own output is discarded, not the -# caller's. Reads JSON from stdin. -jq_has() { jq -e "$@" >/dev/null 2>&1; } - -# jq_lacks FILTER... - true iff the jq filter yields no truthy value (selects nothing, or only false/null). -# `jq -e` exits 1 (last output false/null) or 4 (no output at all) for the "lacks" cases, 0 for a truthy -# match, and 2/3/5 for a real error (malformed filter or input), which is propagated so the calling assert -# fails loudly. The `|| rc=$?` keeps jq in a list (exempt from set -e) so a non-zero exit captures rc instead -# of aborting. Only stdout is discarded - jq's stderr is kept so a real error shows its diagnostic. -jq_lacks() { local rc=0; jq -e "$@" >/dev/null || rc=$?; case "$rc" in 0) return 1 ;; 1|4) return 0 ;; *) return "$rc" ;; esac; } - -check_ruleset() { # name expected-merge-method expect-linear(true/false) - local name="$1" method="$2" linear="$3" id rs - id="$(ruleset_id "$name")" - if [[ -z "$id" ]]; then fail "ruleset '$name' missing"; return; fi - rs="$(gh api "repos/$REPO/rulesets/$id")" - assert "ruleset '$name' active" \ - test "$(jq -r '.enforcement' <<<"$rs")" = active - assert "'$name' merge method = $method" \ - test "$(jq -r '.rules[] | select(.type=="pull_request") | .parameters.allowed_merge_methods | join(",")' <<<"$rs")" = "$method" - assert "'$name' requires signed commits" \ - jq_has '.rules[] | select(.type=="required_signatures")' <<<"$rs" - assert "'$name' strict status policy off" \ - test "$(jq -r '.rules[] | select(.type=="required_status_checks") | .parameters.strict_required_status_checks_policy' <<<"$rs")" = false - # shellcheck disable=SC2016 # $c is a jq variable (--arg c), not a shell expansion - assert "'$name' requires '$REQUIRED_CHECK'" \ - jq_has --arg c "$REQUIRED_CHECK" '.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[] | select(.context==$c)' <<<"$rs" - if [[ "$linear" == "true" ]]; then - assert "'$name' requires linear history" \ - jq_has '.rules[] | select(.type=="required_linear_history")' <<<"$rs" - else - # main must NOT require linear history - it would block the develop -> main merge-commit promotion. - assert "'$name' does not require linear history" \ - jq_lacks '.rules[] | select(.type=="required_linear_history")' <<<"$rs" - fi -} - -# gh_ok ENDPOINT... - true iff the gh api call succeeds (2xx, including 204). Output and errors are -# discarded, so it is safe to pass to `assert`. -gh_ok() { gh api "$@" >/dev/null 2>&1; } - -check_settings() { - local s; s="$(gh api "repos/$REPO")" - # Drive every assertion from settings.json, so the check covers exactly the applied desired state and - # never drifts from the file (add a key there and it is audited here automatically). - local key want got - while IFS=$'\t' read -r key want; do - # shellcheck disable=SC2016 # $k is a jq variable (--arg k), not a shell expansion - got="$(jq -r --arg k "$key" '.[$k]' <<<"$s")" - assert "setting $key = $want" test "$got" = "$want" - done < <(jq -r 'to_entries[] | "\(.key)\t\(.value)"' "$DIR/settings.json") -} - -check_security() { - # apply enables both; audit that they are still on. vulnerability-alerts returns 204 when enabled and - # 404 when disabled; automated-security-fixes returns { "enabled": true/false }. - assert "Dependabot vulnerability alerts enabled" gh_ok "repos/$REPO/vulnerability-alerts" - assert "Dependabot automated security updates enabled" \ - jq_has '.enabled == true' < <(gh api "repos/$REPO/automated-security-fixes") -} - -check_secrets() { - # --paginate: the secrets endpoints page at 30, so without it a repo with many secrets could miss a - # required name and report a false failure. An API/auth error FAILs fast (the required secrets cannot be - # verified, so reporting "matches" would be wrong) - distinct from a genuinely missing secret, which also - # FAILs. gh prints its own error (stderr not suppressed) so the cause is actionable. - local actions deps - if ! actions="$(gh api --paginate "repos/$REPO/actions/secrets" --jq '.secrets[].name')"; then - fail "could not list Actions secrets (API error - cannot verify required secrets)"; return - fi - if ! deps="$(gh api --paginate "repos/$REPO/dependabot/secrets" --jq '.secrets[].name')"; then - fail "could not list Dependabot secrets (API error - cannot verify required secrets)"; return - fi - for s in "${REQUIRED_ACTIONS_SECRETS[@]}"; do - assert "actions secret $s present" grep -qx "$s" <<<"$actions" - done - for s in "${REQUIRED_DEPENDABOT_SECRETS[@]}"; do - assert "dependabot secret $s present" grep -qx "$s" <<<"$deps" - done -} - -check_app() { - # Best-effort: confirm a GitHub App installation backs the merge-bot automation. A precise check - # requires app-level auth; presence of the App secrets above is the practical proxy. - if gh api "repos/$REPO/installation" >/dev/null 2>&1; then - pass "a GitHub App is installed on the repo" - else - note "could not confirm App installation via this token (verify the merge-bot App is installed)" - fi -} - -cmd_check() { - echo "Validating repository configuration for $REPO" - check_ruleset develop squash true - check_ruleset main merge false - check_settings - check_security - check_secrets - check_app - # Not checkable via gh api beyond name presence: that DOCKER_HUB_ACCESS_TOKEN is valid and has push - # access to the per-product + shared-base Docker Hub repos. Verify it by hand in the Docker Hub account. - # The repo list is derived from Make/Matrix.json: docker.io/ptr727/ for each product image plus the - # shared bases nx-base + nx-base-lsio (currently: dwspectrum[-lsio], nxgo[-lsio], nxmeta[-lsio], - # nxwitness[-lsio], wisenetwave[-lsio], nx-base, nx-base-lsio). - note "verify manually: Docker Hub access token valid with push to docker.io/ptr727/ + nx-base + nx-base-lsio" - if [[ "$FAILED" -ne 0 ]]; then echo "Configuration drift detected."; exit 1; fi - echo "Configuration matches." -} - -case "${1:-check}" in - apply) cmd_apply ;; - check) cmd_check ;; - *) echo "usage: $0 [apply|check]" >&2; exit 2 ;; -esac diff --git a/repo-config/ruleset-develop.json b/repo-config/ruleset-develop.json deleted file mode 100644 index daf7dd46..00000000 --- a/repo-config/ruleset-develop.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "develop", - "target": "branch", - "enforcement": "active", - "conditions": { - "ref_name": { - "include": ["refs/heads/develop"], - "exclude": [] - } - }, - "rules": [ - { "type": "deletion" }, - { "type": "non_fast_forward" }, - { "type": "required_linear_history" }, - { "type": "required_signatures" }, - { - "type": "pull_request", - "parameters": { - "allowed_merge_methods": ["squash"], - "dismiss_stale_reviews_on_push": true, - "require_code_owner_review": false, - "require_last_push_approval": false, - "required_approving_review_count": 0, - "required_review_thread_resolution": true - } - }, - { - "type": "required_status_checks", - "parameters": { - "do_not_enforce_on_create": false, - "strict_required_status_checks_policy": false, - "required_status_checks": [ - { "context": "Check pull request workflow status job", "integration_id": 15368 } - ] - } - }, - { - "type": "copilot_code_review", - "parameters": { - "review_draft_pull_requests": true, - "review_on_push": true - } - } - ] -} diff --git a/repo-config/ruleset-main.json b/repo-config/ruleset-main.json deleted file mode 100644 index 0864a0e1..00000000 --- a/repo-config/ruleset-main.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "main", - "target": "branch", - "enforcement": "active", - "conditions": { - "ref_name": { - "include": ["refs/heads/main"], - "exclude": [] - } - }, - "rules": [ - { "type": "deletion" }, - { "type": "non_fast_forward" }, - { "type": "required_signatures" }, - { - "type": "pull_request", - "parameters": { - "allowed_merge_methods": ["merge"], - "dismiss_stale_reviews_on_push": true, - "require_code_owner_review": false, - "require_last_push_approval": false, - "required_approving_review_count": 0, - "required_review_thread_resolution": true - } - }, - { - "type": "required_status_checks", - "parameters": { - "do_not_enforce_on_create": false, - "strict_required_status_checks_policy": false, - "required_status_checks": [ - { "context": "Check pull request workflow status job", "integration_id": 15368 } - ] - } - }, - { - "type": "copilot_code_review", - "parameters": { - "review_draft_pull_requests": true, - "review_on_push": true - } - } - ] -} diff --git a/repo-config/settings.json b/repo-config/settings.json deleted file mode 100644 index fc373efd..00000000 --- a/repo-config/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "allow_squash_merge": true, - "allow_merge_commit": true, - "allow_rebase_merge": false, - "allow_auto_merge": true, - "delete_branch_on_merge": false -} diff --git a/version.json b/version.json index 8b44c81e..4dba1c3b 100644 --- a/version.json +++ b/version.json @@ -1,10 +1,10 @@ -{ - "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "2.15", - "publicReleaseRefSpec": [ - "^refs/heads/main$" - ], - "nugetPackageVersion": { - "semVer": 2 - } -} +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "2.15", + "publicReleaseRefSpec": [ + "^refs/heads/main$" + ], + "nugetPackageVersion": { + "semVer": 2 + } +}