This commit is contained in:
Louis Seubert 2026-02-12 21:18:29 +01:00
commit f1d7d6accc
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
33 changed files with 1676 additions and 0 deletions

419
.editorconfig Normal file
View file

@ -0,0 +1,419 @@
root = true
[*]
indent_style = tab
indent_size = 4
tab_width = 4
end_of_line = lf
insert_final_newline = false
trim_trailing_whitespace = true
max_line_length = 120
[*.{md,json,yaml,yml}]
indent_size = 2
indent_style = space
trim_trailing_whitespace = false
[*.{csproj,props,targets,slnx}]
indent_size = 2
indent_style = space
[nuget.config]
indent_size = 2
indent_style = space
#### .NET Coding Conventions ####
[*.{cs,vb}]
# Organize usings
file_header_template = Copyright (c) The Geekeey Authors\nSPDX-License-Identifier: EUPL-1.2
dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true
dotnet_diagnostic.IDE0005.severity = suggestion # https://github.com/dotnet/roslyn/issues/41640
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
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
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_diagnostic.IDE0270.severity = none
dotnet_style_coalesce_expression = true # IDE0029,IDE0030,IDE0270
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_compound_assignment = true
dotnet_diagnostic.IDE0045.severity = suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true # IDE0045
dotnet_diagnostic.IDE0046.severity = suggestion
dotnet_style_prefer_conditional_expression_over_return = true # IDE0046
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_namespace_match_folder = false # resharper: resharper_check_namespace_highlighting
# Field preferences
dotnet_style_readonly_field = true
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# ReSharper preferences
resharper_wrap_object_and_collection_initializer_style = chop_always
resharper_check_namespace_highlighting = none
resharper_csharp_wrap_lines = false
#### C# Coding Conventions ####
[*.cs]
# var preferences
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
csharp_style_var_elsewhere = true
# Expression-bodied members
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
# Code-block preferences
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_inlined_variable_declaration = true
csharp_style_pattern_local_over_anonymous_function = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_range_operator = true
csharp_style_throw_expression = true
dotnet_diagnostic.IDE0058.severity = suggestion
csharp_style_unused_value_assignment_preference = discard_variable # IDE0058
csharp_style_unused_value_expression_statement_preference = discard_variable # IDE0058
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace
# 'namespace' preferences
csharp_style_namespace_declarations = file_scoped
# 'constructor' preferences
csharp_style_prefer_primary_constructors = false
#### C# Formatting Rules ####
[*.cs]
# New line preferences
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
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
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
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### .NET Naming styles ####
[*.{cs,vb}]
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interfaces.required_modifiers =
dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.enums.applicable_kinds = enum
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enums.required_modifiers =
dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers =
dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.methods.applicable_kinds = method
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.methods.required_modifiers =
dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
dotnet_naming_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers =
dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.properties.applicable_kinds = property
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.properties.required_modifiers =
dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.events.applicable_kinds = event
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.events.required_modifiers =
# local
dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
dotnet_naming_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers =
dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers =
dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
dotnet_naming_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
dotnet_naming_symbols.local_constants.required_modifiers = const
# private
dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_constant_fields.required_modifiers = const
# public
dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_constant_fields.required_modifiers = const
# others
dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_style.ipascalcase.required_prefix = I
dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.capitalization = pascal_case
dotnet_naming_style.tpascalcase.required_prefix = T
dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.capitalization = pascal_case
dotnet_naming_style._camelcase.required_prefix = _
dotnet_naming_style._camelcase.required_suffix =
dotnet_naming_style._camelcase.word_separator =
dotnet_naming_style._camelcase.capitalization = camel_case
dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case
[*.{cs,vb}]
dotnet_analyzer_diagnostic.category-style.severity = warning
dotnet_analyzer_diagnostic.category-design.severity = warning
dotnet_analyzer_diagnostic.category-globalization.severity = notice
dotnet_analyzer_diagnostic.category-naming.severity = warning
dotnet_analyzer_diagnostic.category-performance.severity = warning
dotnet_analyzer_diagnostic.category-reliability.severity = warning
dotnet_analyzer_diagnostic.category-security.severity = warning
dotnet_analyzer_diagnostic.category-usage.severity = warning
dotnet_analyzer_diagnostic.category-maintainability.severity = warning
dotnet_diagnostic.CA1716.severity = none # Identifiers should not match keywords
dotnet_diagnostic.CA1816.severity = suggestion # Dispose methods should call SuppressFinalize
dotnet_diagnostic.CA1848.severity = none # Use the LoggerMessage delegates
dotnet_diagnostic.IDE0210.severity = none # Use top-level statements

View file

@ -0,0 +1,16 @@
name: example
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
do-stuff:
runs-on: debian-latest
steps:
- name: Checkout repository
uses: https://code.geekeey.de/actions/core-test/module/checkout@master
- name: Do stuff
run: |
echo "Doing stuff..."
ls -al

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
artifacts/
*.DotSettings.user

37
Directory.Build.props Normal file
View file

@ -0,0 +1,37 @@
<Project>
<PropertyGroup Condition="'$(ArtifactsPath)' == ''">
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
</PropertyGroup>
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
</PropertyGroup>
<PropertyGroup>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<WarningsAsErrors>nullable</WarningsAsErrors>
<WarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</WarningsAsErrors>
</PropertyGroup>
<PropertyGroup Label="NuGet Package Info">
<Authors>The Geekeey Team</Authors>
<Copyright>Copyright (c) The Geekeey Team 2026</Copyright>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.Gitea" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup>
<NuGetAuditLevel>moderate</NuGetAuditLevel>
<NuGetAuditMode>all</NuGetAuditMode>
</PropertyGroup>
</Project>

2
Directory.Build.targets Normal file
View file

@ -0,0 +1,2 @@
<Project>
</Project>

16
Directory.Packages.props Normal file
View file

@ -0,0 +1,16 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<GlobalPackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" Version="5.0.0-1.25277.114" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.SourceLink.Gitea" Version="10.0.102" />
<PackageVersion Include="TUnit" Version="1.11.51" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.2" />
<PackageVersion Include="System.CommandLine" Version="2.0.2" />
</ItemGroup>
</Project>

4
core.slnx Normal file
View file

@ -0,0 +1,4 @@
<Solution>
<Project Path="src/core.next/core.next.csproj" />
<Project Path="src/core/core.csproj" />
</Solution>

11
global.json Normal file
View file

@ -0,0 +1,11 @@
{
"$schema": "https://www.schemastore.org/global.json",
"sdk": {
"version": "10.0.0",
"rollForward": "latestMinor"
},
"msbuild-sdks": {},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}

View file

@ -0,0 +1,13 @@
# artifact-pull
## Usage
```yml
uses: actions/core/module/artifact-pull@1.0.0
with:
name: artifact
pattern: |
build/**/*
!build/exclude-*
run-id: 0001
```

View file

@ -0,0 +1,38 @@
# SPDX-License-Identifier: EUPL-1.2
name: "Release"
description: "Pull build artifacts from the pipeline artifact storage"
author: "Louis Seubert"
inputs:
name:
required: true
description: >
The name of the artifact to download. The name is used to identify the artifact in the
artifact storage and must be unique within the repository.
pattern:
required: false
description: >
Paths of files which will be extracted from the artifact. Can be multiple lines with file system globbing
patterns. A line starting with ! will negate the pattern and act as exclude pattern. If this input is not provided,
all files of the artifact will be extracted.
run-id:
required: false
description: >
The id of the workflow run from which the artifact should be downloaded. If this input is not provided, the
artifact from the current workflow run will be downloaded.
default: "${{ github.run_id }}"
runs:
using: 'docker'
image: 'code.geekeey.de/actions/core:1.0.0'
args:
- '--server'
- '${{ github.server_url }}'
- '--token'
- '${{ github.token }}'
- 'artifact'
- 'pull'
- '--name'
- '${{ inputs.name }}'
- '--pattern'
- '${{ inputs.pattern }}'
- '--run-id'
- '${{ inputs.run-id }}'

View file

@ -0,0 +1,16 @@
# artifact-push
## Usage
```yml
uses: actions/core/module/artifact-push@1.0.0
with:
name: artifact
path: |
build/**/*
!build/exclude-*
compression: 0
retention-days: 30
overwrite: false
include-hidden-files: false
```

View file

@ -0,0 +1,54 @@
# SPDX-License-Identifier: EUPL-1.2
name: "Release"
description: "Push build artifacts to the pipeline artifact storage"
author: "Louis Seubert"
inputs:
name:
required: true
description: >
The name of the artifact to upload. The name is used to identify the artifact in the
artifact storage and must be unique within the repository.
pattern:
required: true
description: >
Paths to files which will be uploaded as artifact. Can be multiple lines with file system
globbing patterns. A line starting with ! will negate the pattern and act as exclude
pattern.
retention-days:
required: false
description: >
The number of days to keep the artifact in the artifact storage. After this period, the artifact will be
automatically deleted. The default is 0 days which is the defaul value of the runner.
default: 0
overwrite:
required: false
description: >
Whether an artifact with the same name should be overwritten if it already exists in the artifact storage.
The default is false.
default: "false"
include-hidden:
required: false
description: >
Whether hidden files should be included in the artifact. A hidden file is a file which
starts with a dot (.) and is not the current or parent directory. Default is false.
default: "false"
runs:
using: 'docker'
image: 'code.geekeey.de/actions/core:1.0.0'
args:
- '--server'
- '${{ github.server_url }}'
- '--token'
- '${{ github.token }}'
- 'artifact'
- 'push'
- '--name'
- '${{ inputs.name }}'
- '--pattern'
- '${{ inputs.pattern }}'
- '--retention-days'
- '${{ inputs.retention-days }}'
- '--overwrite'
- '${{ inputs.overwrite }}'
- '--include-hidden'
- '${{ inputs.include-hidden }}'

11
module/checkout/README.md Normal file
View file

@ -0,0 +1,11 @@
# checkout
## Usage
```yml
uses: actions/core/module/checkout@1.0.0
with:
repository: ${{ github.server_url }}/${{ github.repository }}.git
path: ${{github.workspace}}
ref: ${{ github.ref || github.sha }}
```

View file

@ -0,0 +1,35 @@
# SPDX-License-Identifier: EUPL-1.2
name: "Checkout"
description: "Checkout a Git repository at a particular version"
author: "Louis Seubert"
inputs:
repository:
required: false
description: >
The path to the repository to checkout. Must be accessible with the
pipeline token or publicly. Can be either an HTTPS or SSH URL.
default: "${{ github.server_url }}/${{ github.repository }}.git"
path:
required: false
description: >
The directory to checkout the repository into. If the directory does not
exist, it will be created.
default: "${{ github.workspace }}"
ref:
required: false
description: >
The branch, tag or SHA to checkout. When checking out the repository that
triggered a workflow, this defaults to the reference or SHA for that
event. Otherwise, uses the default branch.
default: "${{ github.ref || github.sha }}"
runs:
using: 'docker'
image: 'docker://code.geekeey.de/actions/core:1.0.0'
args:
- 'checkout'
- '--repository'
- '${{ inputs.repository }}'
- '--path'
- '${{ inputs.path }}'
- '--reference'
- '${{ inputs.ref }}'

17
module/release/README.md Normal file
View file

@ -0,0 +1,17 @@
# release
## Usage
```yml
uses: actions/core/module/release@1.0.0
with:
repository: ${{ github.server_url }}/${{ github.repository }}
version: ${{ github.ref_name }}
draft: false # or pattern to match agains version e.g. '\d+\.\d+\.\d+'
prerelease: false # or pattern to match agains version e.g. '.*-rc\.\d+'
title: Release ${{ github.ref_name }}
notes: ${{ steps.changelog.outputs.changelog }}
attachments: |
${{ github.workspace }}/build/*.zip
!${{ github.workspace }}/build/exclude-*.zip
```

69
module/release/action.yml Normal file
View file

@ -0,0 +1,69 @@
# SPDX-License-Identifier: EUPL-1.2
name: "Release"
description: "Create a release from a Git tag and attache file artifacts"
author: "Louis Seubert"
inputs:
repository:
required: false
description: >
The owner and repository name seperated by a slash of the repository in which
the release should be created. The repository must be on the same instance as
the action is running on.
default: "${{ github.server_url }}/${{ github.repository }}"
version:
required: false
description: >
The tag name of the git tag for which the release should be created and to which
the artifacts should be attached to.
default: "${{ github.ref_name }}"
draft:
required: false
description: >
A boolean value or a regex pattern which will be evaluated against the version
to determine if the created release will be saved as draft.
default: "false"
prerelease:
required: false
description: >
A boolean value or a regex pattern which will be evaluated against the version
to determine if the created release will be marked as pre-release.
default: "false"
title:
required: false
description: >
The title of the created release. Leave this empty to use the label of the
referenced git tag.
notes:
required: false
description: >
The message of the created release. Leave this empty to use the notes of the
referenced git tag.
attachments:
required: false
description: >
Paths to files which will be attached to the release. Can be multiple lines
with file system globbing patterns. A line starting with ! will negate the
pattern and act as exclude pattern.
runs:
using: 'docker'
image: 'code.geekeey.de/actions/core:1.0.0'
args:
- '--server'
- '${{ github.server_url }}'
- '--token'
- '${{ github.token }}'
- 'release'
- '--repository'
- '${{ inputs.repository }}'
- '--version'
- '${{ inputs.version }}'
- '--draft'
- '${{ inputs.draft }}'
- '--prerelease'
- '${{ inputs.prerelease }}'
- '--title'
- '${{ inputs.title }}'
- '--notes'
- '${{ inputs.notes }}'
- '--attachments'
- '${{ inputs.attachments }}'

19
nuget.config Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
<add key="defaultPushSource" value="geekeey" />
</config>
<packageSources>
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="geekeey" value="https://code.geekeey.de/api/packages/geekeey/nuget/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget">
<package pattern="*" />
</packageSource>
<packageSource key="geekeey">
<package pattern="Geekeey.*" />
</packageSource>
</packageSourceMapping>
</configuration>

View file

@ -0,0 +1,105 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.CommandLine;
using System.Text;
namespace Geekeey.Actions.Core.Commands;
internal sealed class Checkout : Command
{
#pragma warning disable format // @formatter:off
private static readonly Option<Uri> Repository = new("--repository") { Required = true };
private static readonly Option<DirectoryInfo> Destination = new("--path") { Required = true };
private static readonly Option<string> Reference = new("--reference") { Required = true };
#pragma warning restore format // @formatter:on
internal Checkout() : base("checkout")
{
Add(Repository);
Add(Destination);
Add(Reference);
SetAction(HandleAsync);
}
private async Task<int> HandleAsync(ParseResult result, CancellationToken cancellationToken)
{
var server = result.GetRequiredValue(Program.Server);
var access = result.GetRequiredValue(Program.Token);
var workspace = result.GetRequiredValue(Destination);
await $"git init -q {workspace.FullName}";
await $"git -C {workspace.FullName} config set --local protocol.version 2";
await $"git -C {workspace.FullName} config set --local gc.auto 0";
var repository = result.GetRequiredValue(Repository);
if (!repository.IsAbsoluteUri)
{
// relative repository urls are resolved against the server url
repository = new Uri(server, repository);
await $"git -C {workspace.FullName} config set --local --append url.{repository}.insteadOf git@{repository.Host}";
await $"git -C {workspace.FullName} config set --local --append url.{repository}.insteadOf ssh://git@{repository.Host}";
await $"git -C {workspace.FullName} config set --local --append url.{repository}.insteadOf git://{repository.Host}";
var header = $"Authorization: Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token::${access}"))}";
await $"git -C {workspace.FullName} config set --local http.{repository}.extraheader '{header}'";
}
var origin = "origin";
var reference = result.GetRequiredValue(Reference);
await $"git -C {workspace.FullName} remote add {origin} {repository}";
var list = (await $"git -C {workspace.FullName} ls-remote {origin} {reference}").Split('\t');
if (list.Length < 2)
{
throw new InvalidOperationException("git ls-remote resolved nothing");
}
var @sha = list[0].Trim();
var @ref = list[1].Trim();
if (@ref.TryCutPrefix("refs/heads/", out var name))
{
var remote = $"refs/remotes/{origin}/{name}";
var branch = name;
await $"git -C {workspace.FullName} fetch --no-tags --prune --no-recurse-submodules --depth=1 {origin} +{@sha}:{remote}";
await $"git -C {workspace.FullName} checkout --force -B {branch} {remote}";
}
else if (@ref.TryCutPrefix("refs/pull/", out name))
{
var remote = $"refs/remotes/pull/{name}";
// Best-effort parity with the Go code's env.BaseRef/env.HeadRef:
var baseRef = Environment.GetEnvironmentVariable("GITHUB_BASE_REF");
var headRef = Environment.GetEnvironmentVariable("GITHUB_HEAD_REF");
var branch =
!string.IsNullOrEmpty(baseRef) ? baseRef :
!string.IsNullOrEmpty(headRef) ? headRef :
throw new InvalidOperationException("pull request can not find base ref for branch");
await $"git -C {workspace.FullName} fetch --no-tags --prune --no-recurse-submodules --depth=1 {origin} +{@sha}:{remote}";
await $"git -C {workspace.FullName} checkout --force -B {branch} {branch}";
}
else if (@ref.TryCutPrefix("refs/tags/", out name))
{
var remote = $"refs/tags/{name}";
await $"git -C {workspace.FullName} fetch --no-tags --prune --no-recurse-submodules --depth=1 {origin} +{@sha}:{remote}";
await $"git -C {workspace.FullName} checkout --force {remote}";
}
else
{
await $"git -C {workspace.FullName} fetch --no-tags --prune --no-recurse-submodules --depth=1 {origin} {@ref}";
await $"git -C {workspace.FullName} checkout --force {@ref}";
}
return 0;
}
}

51
src/core.next/Program.cs Normal file
View file

@ -0,0 +1,51 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.CommandLine;
using System.CommandLine.Parsing;
using Geekeey.Actions.Core.Commands;
namespace Geekeey.Actions.Core;
internal sealed class Program : RootCommand
{
#pragma warning disable format // @formatter:off
public static readonly Option<Uri> Server = new GitHubContextOption<Uri>("--server", env: "GITHUB_SERVER_URL"){CustomParser = Uri};
public static readonly Option<string> Token = new GitHubContextOption<string>("--token", env: "GITHUB_TOKEN");
#pragma warning restore format // @formatter:on
public static Uri? Uri(ArgumentResult result)
{
string value;
if (!result.Tokens.Any())
{
value = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL")!;
}
else
{
value = result.Tokens.Single().Value;
}
if (!System.Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uri))
{
result.AddError("Not a valid URI.");
return null;
}
return uri;
}
private Program()
{
Add(Server);
Add(Token);
Add(new Checkout());
}
private static Task<int> Main(string[] args)
{
return new Program().Parse(args).InvokeAsync();
}
}

View file

@ -0,0 +1,118 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
namespace Geekeey.Actions.Core;
internal static class ShellLikeString
{
public static TaskAwaiter<string> GetAwaiter(this string command)
{
return RunProcessAsync(command).GetAwaiter();
}
private static async Task<string> RunProcessAsync(string command)
{
var strings = ShellSplit(command);
var psi = new ProcessStartInfo
{
FileName = strings.First(),
ArgumentList =
{
strings.Skip(1)
},
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using var process = Process.Start(psi)!;
var stdout = process.StandardOutput.ReadToEndAsync();
var stderr = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var outputs = await Task.WhenAll(stdout, stderr);
if (process.ExitCode is not 0)
{
throw new ProcessExitException($"Process exited with code {process.ExitCode}: {stderr}");
}
return outputs[0];
}
private static List<string> ShellSplit(string input)
{
var args = new List<string>();
if (string.IsNullOrEmpty(input))
{
return args;
}
var current = new StringBuilder();
var state = ShellSplitState.None;
foreach (var c in input)
{
if (state.HasFlag(ShellSplitState.Escape))
{
current.Append(c);
state &= ~ShellSplitState.Escape;
}
else
{
switch (c)
{
case '\\':
state |= ShellSplitState.Escape;
break;
case '\'' when !state.HasFlag(ShellSplitState.InDoubleQuote):
state ^= ShellSplitState.InSingleQuote;
break;
case '\"' when !state.HasFlag(ShellSplitState.InSingleQuote):
state ^= ShellSplitState.InDoubleQuote;
break;
default:
if (char.IsWhiteSpace(c) && !state.HasFlag(ShellSplitState.InSingleQuote) && !state.HasFlag(ShellSplitState.InDoubleQuote))
{
if (current.Length > 0)
{
args.Add(current.ToString());
current.Clear();
}
}
else
{
current.Append(c);
}
break;
}
}
}
if (current.Length > 0)
{
args.Add(current.ToString());
}
return args;
}
[Flags]
private enum ShellSplitState
{
None,
InSingleQuote,
InDoubleQuote,
Escape,
}
private sealed class ProcessExitException(string message) : Exception(message);
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
internal static partial class Extensions
{
public static void Add<T>(this ICollection<T> collection, IEnumerable<T> items)
{
foreach (var item in items)
{
collection.Add(item);
}
}
}

View file

@ -0,0 +1,17 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
internal static partial class Extensions
{
public static bool TryCutPrefix(this string value, string prefix, out string rest)
{
if (value.StartsWith(prefix, StringComparison.Ordinal))
{
rest = value[prefix.Length..];
return true;
}
rest = string.Empty;
return false;
}
}

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<RootNamespace>Geekeey.Actions.Core</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<ContainerRegistry>code.geekeey.de</ContainerRegistry>
<ContainerRepository>actions/core</ContainerRepository>
<ContainerImageTag>1.0.0</ContainerImageTag>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,136 @@
using System.CommandLine;
using System.IO.Compression;
using Geekeey.Core.Properties;
using Microsoft.Extensions.FileSystemGlobbing;
namespace Geekeey.Core.Commands;
internal sealed class Artifact : Command
{
public Artifact() : base("artifact", Resources.ArtifactCommandDescription)
{
Add(new Pull());
Add(new Push());
}
private static async Task CreateArtifactFromDirectoryAsync(Stream stream, DirectoryInfo directory, Matcher matcher, CancellationToken cancellationToken)
{
await using var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true);
var files = matcher.GetResultsInFullPath(directory.FullName);
foreach (var file in files)
{
cancellationToken.ThrowIfCancellationRequested();
var path = Path.GetRelativePath(directory.FullName, file);
var name = path.Replace(Path.DirectorySeparatorChar, '/');
var entry = archive.CreateEntry(name, CompressionLevel.Optimal);
entry.LastWriteTime = File.GetLastWriteTime(file);
await using var zos = await entry.OpenAsync(cancellationToken);
await using var fis = File.OpenRead(file);
await fis.CopyToAsync(zos, cancellationToken);
}
}
private static async Task ExtractArtifactToDirectoryAsync(Stream stream, DirectoryInfo directory, Matcher matcher, CancellationToken cancellationToken)
{
await using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true);
var entries = archive.Entries.Where(entry => matcher.Match(entry.FullName).HasMatches);
foreach (var entry in entries)
{
cancellationToken.ThrowIfCancellationRequested();
var path = Path.Combine(directory.FullName, entry.FullName);
var file = new FileInfo(path);
file.Directory?.Create();
await using var zis = await entry.OpenAsync(cancellationToken);
await using var fos = File.Create(file.FullName);
await zis.CopyToAsync(fos, cancellationToken);
}
}
private sealed class Pull : Command
{
private static readonly Option<string> Name = new("--name")
{
Required = true
};
private static readonly Option<Matcher?> Pattern = new("--pattern")
{
CustomParser = Parsers.Matcher
};
private static readonly Option<int?> RunId = new("--run-id")
{
Required = false
};
public Pull() : base("pull")
{
Add(Name);
Add(Pattern);
Add(RunId);
SetAction(HandleAsync);
}
private Task<int> HandleAsync(ParseResult result, CancellationToken cancellationToken)
{
// Implementation for checkout command goes here.
return Task.FromResult(0);
}
}
private sealed class Push : Command
{
private static readonly Option<string> Name = new("--name")
{
Required = true
};
private static readonly Option<Matcher?> Pattern = new("--pattern")
{
CustomParser = Parsers.Matcher
};
private static readonly Option<bool> IncludeHidden = new("--include-hidden")
{
Required = false
};
private static readonly Option<int> RetentionDays = new("--retention-days")
{
Required = false
};
private static readonly Option<bool> Overwrite = new("--overwrite")
{
Required = false
};
public Push() : base("push")
{
Add(Name);
Add(Pattern);
Add(IncludeHidden);
Add(RetentionDays);
Add(Overwrite);
SetAction(HandleAsync);
}
private Task<int> HandleAsync(ParseResult result, CancellationToken cancellationToken)
{
// Implementation for checkout command goes here.
return Task.FromResult(0);
}
}
}

View file

@ -0,0 +1,53 @@
using System.CommandLine;
using System.Text;
using Geekeey.Core.Properties;
namespace Geekeey.Core.Commands;
internal sealed class Checkout : Command
{
private static readonly Option<Uri> Repository = new("--repository") { Required = true, CustomParser = Parsers.Uri };
private static readonly Option<DirectoryInfo> Destination = new("--path") { Required = true };
private static readonly Option<string> Reference = new("--reference") { Required = true };
private static readonly Option<bool> TreeLessClone = new("--tree-less-clone") { Description = "Use tree-less (blob-less) clone to reduce bandwidth" };
public Checkout() : base("checkout", Resources.CheckoutCommandDescription)
{
Add(Repository);
Add(Destination);
Add(Reference);
Add(TreeLessClone);
SetAction(HandleAsync);
}
private async Task<int> HandleAsync(ParseResult result, CancellationToken cancellationToken)
{
var server = result.GetRequiredValue(Program.Server);
var access = result.GetRequiredValue(Program.Token);
// relative repository urls are resolved against the server url
var repository = result.GetRequiredValue(Repository);
if (!repository.IsAbsoluteUri)
{
repository = new Uri(new Uri(server), repository);
}
var workspace = result.GetRequiredValue(Destination);
await $"git init -q {workspace.FullName}";
await $"git -C {workspace.FullName} config set --local protocol.version 2";
await $"git -C {workspace.FullName} config set --local gc.auto 0";
await $"git -C {workspace.FullName} config set --local --append url.{repository}.insteadOf git@{repository.Host}";
await $"git -C {workspace.FullName} config set --local --append url.{repository}.insteadOf ssh://git@{repository.Host}";
await $"git -C {workspace.FullName} config set --local --append url.{repository}.insteadOf git://{repository.Host}";
var header = $"Authorization: Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes(access))}";
await $"git -C {workspace.FullName} config set --local http.{repository}.extraheader '{header}'";
await $"git -C {workspace.FullName} remote add origin {repository}";
await $"git -C {workspace.FullName} ls-remote origin {result.GetRequiredValue(Reference)}";
return 0;
}
}

View file

@ -0,0 +1,57 @@
using System.CommandLine;
using Geekeey.Core.Properties;
using Microsoft.Extensions.FileSystemGlobbing;
namespace Geekeey.Core.Commands;
internal sealed class Release : Command
{
private static readonly Option<Uri> Repository = new("--repository") { Required = true, CustomParser = Parsers.Uri };
private static readonly Option<string> Version = new("--version") { Required = true };
private static readonly Option<Pattern> Draft = new("--draft") { CustomParser = Parsers.Pattern };
private static readonly Option<Pattern> PreRelease = new("--prerelease") { CustomParser = Parsers.Pattern };
private static readonly Option<string> Title = new("--title");
private static readonly Option<string> Notes = new("--notes");
private static readonly Option<Matcher> Attachments = new("--attachments") { CustomParser = Parsers.Matcher };
public Release() : base("release", Resources.ReleaseCommandDescription)
{
Add(Repository);
Add(Version);
Add(Draft);
Add(PreRelease);
Add(Title);
Add(Notes);
Add(Attachments);
SetAction(HandleAsync);
}
private Task<int> HandleAsync(ParseResult result, CancellationToken cancellationToken)
{
var server = result.GetRequiredValue(Program.Server);
var access = result.GetRequiredValue(Program.Token);
// relative repository urls are resolved against the server url
var repository = result.GetRequiredValue(Repository);
if (!repository.IsAbsoluteUri)
{
repository = new Uri(new Uri(server), repository);
}
var version = result.GetRequiredValue(Version);
var draft = result.GetValue(Draft);
var prerelease = result.GetValue(PreRelease);
var title = result.GetValue(Title);
var notes = result.GetValue(Notes);
var attachments = result.GetValue(Attachments);
// Implementation for checkout command goes here.
return Task.FromResult(0);
}
}

View file

@ -0,0 +1,14 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Core;
/// <summary>
/// Provides access to GitHub Actions environment context from environment variables.
/// </summary>
internal sealed class GitHubEnvironmentContext : IGitHubEnvironmentContext
{
public string? BaseRef => Environment.GetEnvironmentVariable("GITHUB_BASE_REF");
public string? HeadRef => Environment.GetEnvironmentVariable("GITHUB_HEAD_REF");
}

View file

@ -0,0 +1,22 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Core;
/// <summary>
/// Provides access to GitHub Actions environment context.
/// </summary>
public interface IGitHubEnvironmentContext
{
/// <summary>
/// Gets the base reference for pull requests.
/// Corresponds to the GITHUB_BASE_REF environment variable.
/// </summary>
string? BaseRef { get; }
/// <summary>
/// Gets the head reference for pull requests.
/// Corresponds to the GITHUB_HEAD_REF environment variable.
/// </summary>
string? HeadRef { get; }
}

102
src/core/Parsers.cs Normal file
View file

@ -0,0 +1,102 @@
using System.CommandLine.Parsing;
using System.Text.RegularExpressions;
using Microsoft.Extensions.FileSystemGlobbing;
namespace Geekeey.Core;
internal static class Parsers
{
public static Uri? Uri(ArgumentResult result)
{
if (!result.Tokens.Any())
{
return null;
}
if (!System.Uri.TryCreate(result.Tokens.Single().Value, UriKind.RelativeOrAbsolute, out var uri))
{
result.AddError("Not a valid URI.");
return null;
}
return uri;
}
public static Pattern? Pattern(ArgumentResult result)
{
if (!result.Tokens.Any())
{
return null;
}
if (bool.TryParse(result.Tokens.Single().Value, out var value))
{
return new Pattern(value);
}
try
{
var regex = new Regex(result.Tokens.Single().Value, RegexOptions.Compiled | RegexOptions.IgnoreCase);
return new Pattern(regex);
}
catch (ArgumentException)
{
result.AddError("Not a valid boolean or regex pattern.");
return null;
}
}
public static Matcher? Matcher(ArgumentResult result)
{
if (!result.Tokens.Any())
{
return null;
}
var matcher = new Matcher();
foreach (var token in result.Tokens)
{
try
{
if (token.Value.StartsWith('!'))
{
matcher.AddExclude(token.Value[1..]);
}
else
{
matcher.AddInclude(token.Value);
}
}
catch (Exception exception)
{
result.AddError($"Not a valid glob pattern: {token.Value}. {exception.Message}");
return null;
}
}
return matcher;
}
}
internal sealed class Pattern
{
private readonly bool _value;
private readonly Regex? _pattern;
public Pattern(bool value)
{
_value = value;
}
public Pattern(Regex pattern)
{
_pattern = pattern;
}
public bool Match(string version)
{
return _pattern?.IsMatch(version) ?? _value;
}
}

26
src/core/Program.cs Normal file
View file

@ -0,0 +1,26 @@
using System.CommandLine;
using Geekeey.Core.Commands;
namespace Geekeey.Core;
internal sealed class Program : RootCommand
{
public static readonly Option<string> Server = new("--server") { Required = true, Recursive = true };
public static readonly Option<string> Token = new("--token") { Required = true, Recursive = true };
private Program()
{
Add(Server);
Add(Token);
Add(new Checkout());
Add(new Release());
Add(new Artifact());
}
public static Task<int> Main(string[] args)
{
var program = new Program();
return program.Parse(args).InvokeAsync();
}
}

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="ArtifactCommandDescription" xml:space="preserve">
<value>X</value>
</data>
<data name="CheckoutCommandDescription" xml:space="preserve">
<value>Checkout a specific version or branch of a git repository.</value>
</data>
<data name="ReleaseCommandDescription" xml:space="preserve">
<value>Release a version to the release page on the repository.</value>
</data>
</root>

90
src/core/Shellify.cs Normal file
View file

@ -0,0 +1,90 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
internal static class Shellify
{
[Flags]
private enum ShellSplitState
{
None = 0,
InSingleQuote = 1 << 0,
InDoubleQuote = 1 << 1,
Escape = 1 << 2
}
/// <summary>
/// Splits a shell-escaped string into arguments, handling quotes and escapes.
/// </summary>
public static List<string> ShellSplit(string input)
{
var args = new List<string>();
if (string.IsNullOrEmpty(input)) return args;
var current = new System.Text.StringBuilder();
var state = ShellSplitState.None;
foreach (char c in input)
{
if (state.HasFlag(ShellSplitState.Escape))
{
current.Append(c);
state &= ~ShellSplitState.Escape;
}
else if (c == '\\')
{
state |= ShellSplitState.Escape;
}
else if (c == '\'' && !state.HasFlag(ShellSplitState.InDoubleQuote))
{
state ^= ShellSplitState.InSingleQuote;
}
else if (c == '"' && !state.HasFlag(ShellSplitState.InSingleQuote))
{
state ^= ShellSplitState.InDoubleQuote;
}
else if (char.IsWhiteSpace(c) && !state.HasFlag(ShellSplitState.InSingleQuote) && !state.HasFlag(ShellSplitState.InDoubleQuote))
{
if (current.Length > 0)
{
args.Add(current.ToString());
current.Clear();
}
}
else
{
current.Append(c);
}
}
if (current.Length > 0)
args.Add(current.ToString());
return args;
}
public static TaskAwaiter<string> GetAwaiter(this string command)
{
return RunProcessAsync(command).GetAwaiter();
}
private static async Task<string> RunProcessAsync(string command)
{
var strings = ShellSplit(command);
var psi = new ProcessStartInfo
{
FileName = strings.First(),
ArgumentList =
{
strings.Skip(1)
},
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using var process = Process.Start(psi);
string output = await process.StandardOutput.ReadToEndAsync();
string error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode != 0)
throw new System.Exception($"Process exited with code {process.ExitCode}: {error}");
return output;
}
}

35
src/core/core.csproj Normal file
View file

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Geekeey.Core</RootNamespace>
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<InvariantGlobalization>true</InvariantGlobalization>
<EnableSdkContainerSupport>true</EnableSdkContainerSupport>
<ContainerFamily>alpine</ContainerFamily>
<ContainerRuntimeIdentifiers>linux-x64;linux-arm64</ContainerRuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup>
<AnalysisMode>Recommended</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" />
<PackageReference Include="System.CommandLine" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Commands\Checkout.messages.resx" ClassName="Checkout">
</EmbeddedResource>
</ItemGroup>
</Project>