├── .all-contributorsrc ├── .dockerignore ├── .editorconfig ├── .github ├── FUNDING.yml ├── actions │ └── build │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── build-and-test.yml │ ├── codeql-analysis.yml │ └── publish.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.Packages.props ├── LICENSE ├── PACKAGES.md ├── README.md ├── dotnet-github-actions-sdk.sln ├── global.json ├── playground └── Octokit.Sandbox │ ├── Octokit.Sandbox.csproj │ └── Program.cs ├── samples ├── Actions.Core.Sample │ ├── Actions.Core.Sample.csproj │ ├── Dockerfile │ ├── GlobalUsings.cs │ ├── Program.cs │ └── action.yml ├── Actions.Glob.Sample │ ├── Actions.Glob.Sample.csproj │ ├── Dockerfile │ ├── GlobalUsings.cs │ ├── Program.cs │ └── action.yml └── Directory.Build.props ├── src ├── Actions.Core │ ├── Actions.Core.csproj │ ├── AnnotationProperties.cs │ ├── Commands │ │ ├── CommandNames.cs │ │ ├── DefaultCommandIssuer.cs │ │ ├── DefaultFileCommandIssuer.cs │ │ ├── ICommandIssuer.cs │ │ └── IFileCommandIssuer.cs │ ├── EnvironmentVariables │ │ ├── Keys.cs │ │ ├── Prefixes.cs │ │ └── Suffixes.cs │ ├── ExitCode.cs │ ├── Extensions │ │ ├── ArgumentNullExceptionExtensions.cs │ │ ├── GenericExtensions.cs │ │ ├── ObjectExtensions.cs │ │ ├── ProcessExtensions.cs │ │ └── ServiceCollectionExtensions.cs │ ├── GlobalUsings.cs │ ├── InputOptions.cs │ ├── Markdown │ │ ├── AlertType.cs │ │ ├── TableColumnAlignment.cs │ │ └── TaskItem.cs │ ├── Output │ │ ├── DefaultConsole.cs │ │ └── IConsole.cs │ ├── README.md │ ├── Services │ │ ├── DefaultCoreService.cs │ │ └── ICoreService.cs │ ├── Summaries │ │ ├── Summary.cs │ │ ├── SummaryImageOptions.cs │ │ ├── SummaryTable.cs │ │ ├── SummaryTableCell.cs │ │ ├── SummaryTableRow.cs │ │ └── SummaryWriteOptions.cs │ └── Workflows │ │ └── Command.cs ├── Actions.Glob │ ├── Actions.Glob.csproj │ ├── DefaultGlobPatternResolver.cs │ ├── DefaultGlobPatternResolverBuilder.cs │ ├── Extensions │ │ ├── ServiceCollectionExtensions.cs │ │ ├── StringExtensions.Files.cs │ │ └── StringExtensions.Results.cs │ ├── GlobResult.cs │ ├── GlobalUsings.cs │ ├── Globber.cs │ ├── IGlobPatternResolver.cs │ ├── IGlobPatternResolverBuilder.cs │ └── README.md ├── Actions.HttpClient │ ├── Actions.HttpClient.csproj │ ├── ClientNames.cs │ ├── Clients │ │ ├── DefaultHttpClient.cs │ │ └── IHttpClient.cs │ ├── Extensions │ │ ├── HttpMethodExtensions.cs │ │ ├── ServiceCollectionExtensions.cs │ │ └── StringExtensions.cs │ ├── Factories │ │ ├── DefaultHttpKeyedClientFactory.cs │ │ └── IHttpCredentialClientFactory.cs │ ├── GlobalUsings.cs │ ├── Handlers │ │ ├── BasicCredentialHandler.cs │ │ ├── BearerCredentialHandler.cs │ │ ├── IRequestHandler.cs │ │ └── PersonalAccessTokenHandler.cs │ ├── Proxy.cs │ ├── README.md │ ├── RequestOptions.cs │ └── TypedResponse.cs ├── Actions.IO │ ├── Actions.IO.csproj │ ├── CopyOptions.cs │ ├── GlobalUsings.cs │ ├── IOperations.cs │ ├── MoveOptions.cs │ ├── Operations.cs │ ├── README.md │ └── Utilities.cs ├── Actions.Octokit │ ├── Actions.Octokit.csproj │ ├── Common │ │ ├── Issue.cs │ │ └── Repository.cs │ ├── Context.cs │ ├── Extensions │ │ └── ServiceCollectionExtensions.cs │ ├── GitHubClientFactory.cs │ ├── GlobalUsings.cs │ ├── Interfaces │ │ ├── Comment.cs │ │ ├── Installation.cs │ │ ├── Owner.cs │ │ ├── PayloadRepository.cs │ │ ├── PullRequest.cs │ │ ├── Sender.cs │ │ ├── WebhookIssue.cs │ │ └── WebhookPayload.cs │ ├── README.md │ └── Serialization │ │ └── SourceGenerationContexts.cs ├── Directory.Build.props ├── Directory.Build.targets └── build │ └── Common.props └── tests ├── Actions.Core.Tests ├── Actions.Core.Tests.csproj ├── Commands │ ├── DefaultCommandIssuerTests.cs │ └── DefaultFileCommandIssuerTests.cs ├── Extensions │ ├── ArgumentNullExceptionExtensionsTests.cs │ ├── GenericExtensionsTests.cs │ ├── ObjectExtensionsTests.cs │ └── ServiceCollectionExtensionsTests.cs ├── GlobalUsings.cs ├── Output │ └── TestConsole.cs ├── Services │ ├── CoreSummaryTestFixture.cs │ ├── CoreSummaryTests.cs │ └── DefaultWorkflowStepServiceTests.cs └── Workflows │ └── CommandTests.cs ├── Actions.Glob.Tests ├── Actions.Glob.Tests.csproj ├── Extensions │ └── StringExtensionTests.cs ├── GlobPatternBuilderTests.cs ├── GlobalUsings.cs └── parent │ ├── README.md │ ├── child │ ├── assets │ │ ├── image.png │ │ └── image.svg │ ├── file.MD │ ├── grandchild │ │ ├── file.md │ │ ├── style.css │ │ └── sub.text │ ├── index.js │ ├── more.md │ └── sample.mtext │ └── file.md ├── Actions.HttpClient.Tests ├── Actions.HttpClient.Tests.csproj ├── Args.cs ├── AuthTests.cs ├── BasicTests.cs ├── GlobalUsings.cs ├── HttpMethodExtensionsTests.cs ├── PostmanEchoGetResponse.cs ├── PostmanEchoResponse.cs ├── ProxyTests.cs ├── RequestData.cs ├── ServiceCollectionExtensionsTests.cs ├── SourceGenerationContext.cs └── StringExtensionsTest.cs ├── Actions.IO.Tests ├── Actions.IO.Tests.csproj ├── CopyOptionsTests.cs ├── GlobalUsings.cs ├── MoveOptionsTests.cs ├── OperationTests.cs └── TempFolderTestFixture.cs ├── Actions.Octokit.Tests ├── Actions.Octokit.Tests.csproj ├── ContextTests.cs ├── GitHubClientFactoryTests.cs ├── GitHubClientTests.cs └── GlobalUsings.cs └── Directory.Build.props /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "baronfel", 12 | "name": "Chet Husk", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/573979?v=4", 14 | "profile": "http://www.chethusk.com", 15 | "contributions": [ 16 | "code" 17 | ] 18 | }, 19 | { 20 | "login": "js6pak", 21 | "name": "js6pak", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/35262707?v=4", 23 | "profile": "https://github.com/js6pak", 24 | "contributions": [ 25 | "code", 26 | "test" 27 | ] 28 | }, 29 | { 30 | "login": "flcdrg", 31 | "name": "David Gardiner", 32 | "avatar_url": "https://avatars.githubusercontent.com/u/384747?v=4", 33 | "profile": "https://david.gardiner.net.au", 34 | "contributions": [ 35 | "code" 36 | ] 37 | }, 38 | { 39 | "login": "fredrikhr", 40 | "name": "Fredrik Høisæther Rasch", 41 | "avatar_url": "https://avatars.githubusercontent.com/u/8759693?v=4", 42 | "profile": "https://thnetii.td.org.uit.no/", 43 | "contributions": [ 44 | "code", 45 | "ideas" 46 | ] 47 | } 48 | ], 49 | "contributorsPerLine": 7, 50 | "skipCi": true, 51 | "repoType": "github", 52 | "repoHost": "https://github.com", 53 | "projectName": "dotnet-github-actions-sdk", 54 | "projectOwner": "IEvangelist" 55 | } 56 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.gitignore 5 | **/.project 6 | **/.settings 7 | **/.toolstarget 8 | **/.vs 9 | **/.vscode 10 | **/*.*proj.user 11 | **/*.dbmdl 12 | **/*.jfm 13 | **/azds.yaml 14 | **/bin 15 | **/charts 16 | **/docker-compose* 17 | **/Dockerfile* 18 | **/node_modules 19 | **/npm-debug.log 20 | **/obj 21 | **/secrets.dev.yaml 22 | **/values.dev.yaml 23 | LICENSE 24 | README.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: IEvangelist 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | description: "A build GitHub Action composite shared workflow." 3 | 4 | inputs: 5 | upload-artifacts: 6 | description: "Whether to upload artifacts" 7 | required: true 8 | default: "true" 9 | run-tests: 10 | description: "Whether to runs tests" 11 | required: true 12 | default: "true" 13 | 14 | runs: 15 | using: "composite" 16 | steps: 17 | - name: Setup .NET 18 | uses: actions/setup-dotnet@v4 19 | with: 20 | global-json-file: global.json 21 | 22 | - name: Install dependencies 23 | shell: bash 24 | run: dotnet restore 25 | 26 | - name: Build 27 | shell: bash 28 | run: dotnet build --configuration Release --no-restore 29 | 30 | - uses: actions/upload-artifact@v4 31 | if: inputs.upload-artifacts == 'true' 32 | with: 33 | name: nuget 34 | path: artifacts/package/release/*.nupkg 35 | 36 | - name: Test 37 | if: inputs.run-tests == 'true' 38 | shell: bash 39 | run: dotnet test --configuration Release --no-restore --filter "Category!=RequiresEnvVar" 40 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # Core GitHub Actions 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | day: "wednesday" 13 | - package-ecosystem: "nuget" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | day: "wednesday" 18 | open-pull-requests-limit: 5 19 | groups: 20 | # Group .NET updates together for solutions. 21 | dotnet: 22 | patterns: 23 | - "*" # Prefer a single PR per solution update. 24 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | workflow_dispatch: 9 | inputs: 10 | reason: 11 | description: 'The reason for running a build?' 12 | required: true 13 | default: 'Manual build' 14 | 15 | jobs: 16 | build: 17 | name: build-${{matrix.os}} 18 | runs-on: ${{ matrix.os }} 19 | env: 20 | DOTNET_CLI_TELEMETRY_OPTOUT: '1' 21 | strategy: 22 | matrix: 23 | os: [ ubuntu-latest, windows-latest, macos-latest ] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | filter: tree:0 30 | 31 | - name: 'Print manual build reason' 32 | if: ${{ github.event_name == 'workflow_dispatch' }} 33 | run: | 34 | echo 'Reason: ${{ github.event.inputs.reason }}' 35 | 36 | - name: Build 37 | uses: ./.github/actions/build 38 | with: 39 | upload-artifacts: ${{ matrix.os == 'ubuntu-latest' }} 40 | 41 | test-samples: 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | with: 47 | fetch-depth: 0 48 | filter: tree:0 49 | 50 | - name: Build 51 | run: | 52 | docker build -f samples/Actions.Core.Sample/Dockerfile . -t actions-core-sample:latest 53 | docker build -f samples/Actions.Glob.Sample/Dockerfile . -t actions-glob-sample:latest 54 | 55 | - uses: ./samples/Actions.Core.Sample 56 | 57 | - uses: ./samples/Actions.Glob.Sample 58 | with: 59 | files: "**/*.cs" 60 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # CodeQL Code Scanning 2 | # Analyses your code for security vulnerabilities and coding errors. 3 | # For most projects, this workflow file will not need changing; you simply need 4 | # to commit it to your repository. You may wish to alter this file to override 5 | # the set of languages analyzed, or to provide custom queries or build logic. 6 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/about-code-scanning 7 | name: code analysis 8 | 9 | on: 10 | push: 11 | branches: [main] 12 | paths: 13 | - '**.cs' 14 | - '**.csproj' 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [main] 18 | paths: 19 | - '**.cs' 20 | - '**.csproj' 21 | schedule: 22 | - cron: '0 7 * * 1' 23 | workflow_dispatch: 24 | inputs: 25 | reason: 26 | description: 'The reason for running a build?' 27 | required: true 28 | default: 'Manual build' 29 | 30 | jobs: 31 | analyze: 32 | name: analyze 33 | runs-on: ubuntu-latest 34 | 35 | permissions: 36 | security-events: write 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | # Override automatic language detection by changing the below list 42 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 43 | language: ['csharp'] 44 | # Learn more... 45 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 46 | 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v4 50 | with: 51 | fetch-depth: 0 52 | filter: tree:0 53 | 54 | - name: 'Print manual build reason' 55 | if: ${{ github.event_name == 'workflow_dispatch' }} 56 | run: | 57 | echo 'Reason: ${{ github.event.inputs.reason }}' 58 | 59 | # Initializes the CodeQL tools for scanning. 60 | - name: Initialize CodeQL 61 | uses: github/codeql-action/init@v3 62 | with: 63 | languages: ${{ matrix.language }} 64 | # If you wish to specify custom queries, you can do so here or in a config file. 65 | # By default, queries listed here will override any specified in a config file. 66 | # Prefix the list here with "+" to use these queries and those in the config file. 67 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 68 | 69 | # ℹ️ Command-line programs to run using the OS shell. 70 | # 📚 https://git.io/JvXDl 71 | 72 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 73 | # and modify them (or add more) to build your code if your project 74 | # uses a compiled language 75 | 76 | - name: Setup .NET 77 | uses: actions/setup-dotnet@v4 78 | with: 79 | global-json-file: global.json 80 | 81 | - name: Autobuild 82 | uses: github/codeql-action/autobuild@v3 83 | 84 | - name: Perform CodeQL Analysis 85 | uses: github/codeql-action/analyze@v3 86 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish nuget 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | filter: tree:0 17 | 18 | - name: Build 19 | uses: ./.github/actions/build 20 | 21 | - name: Publish 22 | run: | 23 | dotnet nuget push --skip-duplicate artifacts/package/release/*.nupkg \ 24 | --source "https://api.nuget.org/v3/index.json" --api-key "${{ secrets.NUGET_API_KEY }}" 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | david.pine.7@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | $(MSBuildThisFileDirectory)artifacts 5 | 6 | latest 7 | strict 8 | embedded 9 | enable 10 | enable 11 | 12 | true 13 | latest 14 | recommended 15 | true 16 | true 17 | true 18 | 19 | @(NoWarn);NU1903 20 | 21 | true 22 | true 23 | 24 | David Pine 25 | © 2022-$([System.DateTime]::Now.ToString('yyyy')) David Pine 26 | MIT 27 | README.md 28 | dotnet;dotnetcore;csharp;github;actions;devops; 29 | https://github.com/IEvangelist/dotnet-github-actions-sdk 30 | https://github.com/IEvangelist/dotnet-github-actions-sdk 31 | git 32 | 33 | true 34 | 35 | 36 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | net9.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 David Pine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PACKAGES.md: -------------------------------------------------------------------------------- 1 | # Toolkit Packages 2 | 3 | The following table tracks the various packages development progress. Each package strives to provide functional equivalence with its corresponding `@actions/toolkit` package, but with the following additional features: 4 | 5 | - **Testable**: The package is designed to be testable, with a clear separation between the core logic and the I/O operations. This allows for easier testing and mocking of the package's behavior. 6 | - **DI friendly**: The package is designed to be dependency injection friendly, allowing for easier mocking and testing of the package's behavior. 7 | - **README.md**: The package has a `README.md` file that describes its usage and behavior. 8 | - **Tests**: The package has a test suite that validates its behavior. 9 | - **Attribution**: The package has a clear attribution to the original `@actions/toolkit` package and any other 3rd party OSS packages that it depends on. 10 | 11 | | `@actions/toolkit` | Package | Exists? | Testable? | DI Friendly? | README? | Tests? | Attribution? | 12 | |--|--|:--:|:--:|:--:|:--:|:--:|:--:| 13 | | `@actions/attest` | `GitHubActions.Toolkit.Attest` | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 14 | | `@actions/cache` | `GitHubActions.Toolkit.Cache` | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 15 | | `@actions/core` | `GitHubActions.Toolkit.Core` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 16 | | `@actions/download-artifact` | `GitHubActions.Toolkit.Artifact` | ✅ | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 17 | | `@actions/exec` | `GitHubActions.Toolkit.Exec` | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 18 | | `@actions/github` | `GitHubActions.Toolkit.Octokit` | ✅ | ✅ | ✅ | ✅ | ✅ | 🔳 | 19 | | `@actions/http-client` | `GitHubActions.Toolkit.HttpClient` | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 20 | | `@actions/io` | `GitHubActions.Toolkit.IO` | ✅ | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 21 | | `@actions/tool-cache` | `GitHubActions.Toolkit.ToolCache` | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 22 | | `@actions/upload-artifact` | `GitHubActions.Toolkit.Artifact` | ✅ | 🔳 | 🔳 | 🔳 | 🔳 | 🔳 | 23 | 24 | **Legend** 25 | 26 | - **✅**: Done 27 | - **🔳**: Not done 28 | 29 | ## Testable 30 | 31 | Each package should strive to be testable, such that consumers can test all aspects of an API surface area with ease. 32 | 33 | ## DI Friendly 34 | 35 | Going hand-in-hand with being testable, each package should strive to be dependency injection friendly, such that consumers register services via an `Add*` extension method on the `IServiceCollection` type. 36 | 37 | ## README.md 38 | 39 | All packages require a `README.md` file that describes its usage and behavior. While they can be similar or derived from the original, it's best to keep these concise as they'll need to be packaged within the NuGet and link to a more thorough doc. 40 | 41 | ## Tests 42 | 43 | All packages require a test suite that validates its behavior. This is a requirement for all packages, as it ensures that the package behaves as expected. Additionally, tests are a great way for consumers to learn how a bit of functionality is intended to behave. 44 | 45 | ## Attribution 46 | 47 | Each package is built atop various other packages, and it's important to give credit where credit is due. This includes the original `@actions/toolkit` package, as well as any other 3rd party OSS packages that the package depends on. 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![dotnet-github-actions-sdk](https://socialify.git.ci/IEvangelist/dotnet-github-actions-sdk/image?description=1&font=Rokkitt&language=1&name=1&owner=1&pattern=Plus&theme=Dark) 4 | 5 | # GitHub Actions Workflow .NET SDK 6 | 7 | [![build-and-test](https://github.com/IEvangelist/dotnet-github-actions-sdk/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/IEvangelist/dotnet-github-actions-sdk/actions/workflows/build-and-test.yml) 8 | [![code analysis](https://github.com/IEvangelist/dotnet-github-actions-sdk/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/IEvangelist/dotnet-github-actions-sdk/actions/workflows/codeql-analysis.yml) 9 | [![publish nuget](https://github.com/IEvangelist/dotnet-github-actions-sdk/actions/workflows/publish.yml/badge.svg)](https://github.com/IEvangelist/dotnet-github-actions-sdk/actions/workflows/publish.yml) 10 | [![NuGet](https://img.shields.io/nuget/v/GitHub.Actions.Core.svg?style=flat)](https://www.nuget.org/packages/GitHub.Actions.Core) 11 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) 12 | 13 | 14 | The .NET equivalent of the official GitHub [actions/toolkit](https://github.com/actions/toolkit) repository, and is currently a work in progress. While there isn't currently 100% feature complete compatibility between these two repositories, that is the eventual goal. 15 | 16 | ## Blog 17 | 18 | [🔗 Hello from the GitHub Actions: Core .NET SDK](https://davidpine.net/blog/github-actions-sdk) 19 | 20 | ## GitHub Actions .NET Toolkit 21 | 22 | The GitHub Actions .NET ToolKit provides a set of packages to make creating actions easier. 23 | 24 | ## Packages 25 | 26 | :heavy_check_mark: [`GitHub.Actions.Core`](src/Actions.Core) 27 | 28 | Provides functions for inputs, outputs, results, logging, secrets and variables. Read more [here](src/Actions.Core) 29 | 30 | ``` 31 | dotnet add package GitHub.Actions.Core 32 | ``` 33 | 34 | For more information, see [📦 GitHub.Actions.Core](https://www.nuget.org/packages/GitHub.Actions.Core). 35 | 36 | :ice_cream: [`GitHub.Actions.Glob`](src/Actions.Glob) 37 | 38 | Provides functions to search for files matching glob patterns. Read more [here](src/Actions.Glob) 39 | 40 | ``` 41 | dotnet add package GitHub.Actions.Glob 42 | ``` 43 | 44 | For more information, see [📦 GitHub.Actions.Glob](https://www.nuget.org/packages/GitHub.Actions.Glob). 45 | 46 | 59 | 60 | :octocat: [`GitHub.Actions.Octokit`](src/Actions.Octokit) 61 | 62 | Provides an Octokit client hydrated with the context that the current action is being run in. Read more [here](src/Actions.Octokit) 63 | 64 | ```bash 65 | dotnet add package GitHub.Actions.Octokit 66 | ``` 67 | 68 | For more information, see [📦 GitHub.Actions.Octokit](https://www.nuget.org/packages/GitHub.Actions.Octokit). 69 | 70 | ## Contributors ✨ 71 | 72 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
Chet Husk
Chet Husk

💻
js6pak
js6pak

💻 ⚠️
David Gardiner
David Gardiner

💻
Fredrik Høisæther Rasch
Fredrik Høisæther Rasch

💻 🤔
87 | 88 | 89 | 90 | 91 | 92 | 93 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 94 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.100", 4 | "rollForward": "major" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /playground/Octokit.Sandbox/Octokit.Sandbox.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | $(NoWarn);IDE0005 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /playground/Octokit.Sandbox/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Actions.Octokit; 5 | using GitHub.Models; 6 | 7 | var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN") 8 | ?? throw new InvalidOperationException("The GITHUB_TOKEN environment variable is required."); 9 | 10 | var client = GitHubClientFactory.Create(token); 11 | 12 | var sevenDaysAgo = DateTimeOffset.UtcNow.AddDays(-7); 13 | 14 | var user = "IEvangelist"; 15 | 16 | var events = await GetEventsAsync(user).ToListAsync(); 17 | 18 | // Select events, grouping on repo and ordering by creation dates 19 | var groupedEvents = events?.GroupBy(e => e?.Repo?.Name) 20 | .Select(e => (e.Key, e.OrderBy(ed => ed?.CreatedAt).ToArray())); 21 | 22 | foreach (var (repo, eventArray) in groupedEvents ?? []) 23 | { 24 | Console.WriteLine($"In {repo}"); 25 | 26 | for (var i = 0; i < eventArray.Length; i++) 27 | { 28 | var @event = eventArray[i]; 29 | if (@event is null or { Payload: null }) 30 | { 31 | continue; 32 | } 33 | 34 | var line = GetEventBulletPointText(@event); 35 | 36 | Console.WriteLine(line); 37 | } 38 | } 39 | 40 | async IAsyncEnumerable GetEventsAsync(string user) 41 | { 42 | var page = 1; 43 | var done = false; 44 | 45 | while (!done) 46 | { 47 | var events = await client.Users[user].Events.GetAsync(config => 48 | { 49 | config.QueryParameters.PerPage = 100; 50 | config.QueryParameters.Page = page; 51 | }); 52 | 53 | if (events is null or { Count: 0 }) 54 | { 55 | done = true; 56 | yield break; 57 | } 58 | 59 | foreach (var @event in events) 60 | { 61 | if (@event is null) 62 | { 63 | continue; 64 | } 65 | 66 | if (@event.CreatedAt < sevenDaysAgo) 67 | { 68 | yield return @event; 69 | } 70 | else 71 | { 72 | done = true; 73 | } 74 | } 75 | 76 | page++; 77 | } 78 | } 79 | 80 | string GetEventBulletPointText(Event @event) 81 | { 82 | if (@event is null or { Payload: null }) 83 | { 84 | return ""; 85 | } 86 | 87 | var payload = @event.Payload; 88 | 89 | var url = payload.Comment?.HtmlUrl ?? payload.Issue?.HtmlUrl; 90 | var details = "TODO: get the details"; 91 | 92 | return $"- [{@event.Type}: {payload.Action} {details}]({url})"; 93 | } 94 | -------------------------------------------------------------------------------- /samples/Actions.Core.Sample/Actions.Core.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(NoWarn);IDE0005 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Actions.Core.Sample/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-noble AS build 2 | ARG TARGETARCH 3 | WORKDIR /source 4 | 5 | COPY *.props *.targets ./ 6 | COPY samples/*.props samples/*.targets ./samples/ 7 | COPY samples/Actions.Core.Sample/Actions.Core.Sample.csproj samples/Actions.Core.Sample/ 8 | COPY src/*.props src/*.targets ./src/ 9 | COPY src/Actions.Core/Actions.Core.csproj src/Actions.Core/ 10 | COPY src/Actions.Octokit/Actions.Octokit.csproj src/Actions.Octokit/ 11 | RUN dotnet restore "samples/Actions.Core.Sample/Actions.Core.Sample.csproj" -a $TARGETARCH 12 | 13 | COPY . . 14 | RUN dotnet publish "samples/Actions.Core.Sample/Actions.Core.Sample.csproj" -a $TARGETARCH --no-restore -o /app 15 | 16 | FROM mcr.microsoft.com/dotnet/runtime:9.0-noble-chiseled 17 | WORKDIR /app 18 | COPY --from=build /app . 19 | USER root 20 | ENTRYPOINT ["/app/Actions.Core.Sample"] 21 | -------------------------------------------------------------------------------- /samples/Actions.Core.Sample/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Microsoft.Extensions.DependencyInjection; 5 | global using Actions.Octokit; 6 | global using Actions.Core.Extensions; 7 | global using Actions.Core.Services; 8 | -------------------------------------------------------------------------------- /samples/Actions.Core.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using var services = new ServiceCollection() 5 | .AddGitHubActionsCore() 6 | .BuildServiceProvider(); 7 | 8 | var core = services.GetRequiredService(); 9 | 10 | try 11 | { 12 | // "who-to-greet" input defined in action metadata file 13 | var nameToGreet = core.GetInput("who-to-greet"); 14 | core.WriteInfo($"Hello {nameToGreet}!"); 15 | await core.SetOutputAsync("time", DateTime.UtcNow.ToString("o")); 16 | 17 | // Get the JSON webhook payload for the event that triggered the workflow 18 | var payload = Context.Current?.Payload?.ToString(); 19 | 20 | core.WriteInfo($"The event payload: {payload}"); 21 | 22 | await core.SetOutputAsync("yesItWorks", "testing/this/out"); 23 | } 24 | catch (Exception ex) 25 | { 26 | core.SetFailed(ex.ToString()); 27 | } 28 | -------------------------------------------------------------------------------- /samples/Actions.Core.Sample/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Hello World' 2 | description: 'Greet someone and record the time' 3 | inputs: 4 | who-to-greet: # id of input 5 | description: 'Who to greet' 6 | required: true 7 | default: 'World' 8 | outputs: 9 | time: # id of output 10 | description: 'The time we greeted you' 11 | runs: 12 | using: docker 13 | image: docker://actions-core-sample:latest 14 | -------------------------------------------------------------------------------- /samples/Actions.Glob.Sample/Actions.Glob.Sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(NoWarn);IDE0005 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Actions.Glob.Sample/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-noble AS build 2 | ARG TARGETARCH 3 | WORKDIR /source 4 | 5 | COPY *.props *.targets ./ 6 | COPY samples/*.props samples/*.targets ./samples/ 7 | COPY samples/Actions.Glob.Sample/Actions.Glob.Sample.csproj samples/Actions.Glob.Sample/ 8 | COPY src/*.props src/*.targets ./src/ 9 | COPY src/Actions.Core/Actions.Core.csproj src/Actions.Core/ 10 | COPY src/Actions.Octokit/Actions.Octokit.csproj src/Actions.Octokit/ 11 | COPY src/Actions.Glob/Actions.Glob.csproj src/Actions.Glob/ 12 | RUN dotnet restore "samples/Actions.Glob.Sample/Actions.Glob.Sample.csproj" -a $TARGETARCH 13 | 14 | COPY . . 15 | RUN dotnet publish "samples/Actions.Glob.Sample/Actions.Glob.Sample.csproj" -a $TARGETARCH --no-restore -o /app 16 | 17 | FROM mcr.microsoft.com/dotnet/runtime:9.0-noble-chiseled 18 | WORKDIR /app 19 | COPY --from=build /app . 20 | USER root 21 | ENTRYPOINT ["/app/Actions.Glob.Sample"] 22 | -------------------------------------------------------------------------------- /samples/Actions.Glob.Sample/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Actions.Core.Extensions; 5 | global using Actions.Core.Services; 6 | global using Actions.Glob; 7 | 8 | global using Microsoft.Extensions.DependencyInjection; 9 | -------------------------------------------------------------------------------- /samples/Actions.Glob.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using var provider = new ServiceCollection() 5 | .AddGitHubActionsCore() 6 | .BuildServiceProvider(); 7 | 8 | var core = provider.GetRequiredService(); 9 | var globber = Globber.Create(core.GetInput("files")); 10 | foreach (var file in globber.GlobFiles()) 11 | { 12 | core.WriteInfo(file); 13 | } -------------------------------------------------------------------------------- /samples/Actions.Glob.Sample/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Globs' 2 | description: 'Show how Actions.Glob works' 3 | inputs: 4 | files: 5 | description: 'Files to print' 6 | required: true 7 | runs: 8 | using: docker 9 | image: docker://actions-glob-sample:latest 10 | -------------------------------------------------------------------------------- /samples/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net9.0 6 | Exe 7 | false 8 | false 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Actions.Core/Actions.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(DefaultTargetFrameworks) 4 | @actions/core 5 | 6 | 7 | 8 | $(DefineConstants);ACTIONS_CORE_ENVIRONMENTVARIABLES_PUBLIC 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Actions.Core/AnnotationProperties.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core; 5 | 6 | /// 7 | /// Inspired by 8 | /// 9 | public readonly record struct AnnotationProperties 10 | { 11 | /// 12 | /// A title for the annotation. 13 | /// 14 | public string? Title { get; init; } 15 | 16 | /// 17 | /// The path of the file for which the annotation should be created. 18 | /// 19 | public string? File { get; init; } 20 | 21 | /// 22 | /// The start line of the annotation. 23 | /// 24 | public int? StartLine { get; init; } 25 | 26 | /// 27 | /// The end line of the annotation. 28 | /// 29 | public int? EndLine { get; init; } 30 | 31 | /// 32 | /// The start column of the annotation. 33 | /// 34 | public int? StartColumn { get; init; } 35 | 36 | /// 37 | /// The end column of the annotation. 38 | /// 39 | public int? EndColumn { get; init; } 40 | 41 | private bool IsEmpty => Equals(default); 42 | 43 | /// 44 | /// Converts the current annotations instance into a readonly dictionary. 45 | /// 46 | public IReadOnlyDictionary ToCommandProperties() 47 | { 48 | if (IsEmpty) 49 | { 50 | return new Dictionary(); 51 | } 52 | 53 | var properties = new Dictionary(); 54 | TryAddProperty(properties, "title", Title); 55 | TryAddProperty(properties, "file", File); 56 | TryAddProperty(properties, "line", StartLine); 57 | TryAddProperty(properties, "endLine", EndLine); 58 | TryAddProperty(properties, "col", StartColumn); 59 | TryAddProperty(properties, "endColumn", EndColumn); 60 | 61 | return properties; 62 | 63 | static void TryAddProperty( 64 | in IDictionary properties, string key, object? value) 65 | { 66 | if (value is not null) 67 | { 68 | properties[key] = value.ToString()!; 69 | } 70 | } 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/Actions.Core/Commands/CommandNames.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Commands; 5 | 6 | internal static class CommandNames 7 | { 8 | /// A command constant string value "set-env". 9 | public static readonly string SetEnv = "set-env"; 10 | /// A command constant string value "add-mask". 11 | public static readonly string AddMask = "add-mask"; 12 | /// A command constant string value "add-path". 13 | public static readonly string AddPath = "add-path"; 14 | /// A command constant string value "echo". 15 | public static readonly string Echo = "echo"; 16 | /// A command constant string value "debug". 17 | public static readonly string Debug = "debug"; 18 | /// A command constant string value "error". 19 | public static readonly string Error = "error"; 20 | /// A command constant string value "warning". 21 | public static readonly string Warning = "warning"; 22 | /// A command constant string value "notice". 23 | public static readonly string Notice = "notice"; 24 | /// A command constant string value "group". 25 | public static readonly string Group = "group"; 26 | /// A command constant string value "endgroup". 27 | public static readonly string EndGroup = "endgroup"; 28 | 29 | // Deprecated 30 | // https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands 31 | public static readonly string SaveState = "save-state"; 32 | public static readonly string SetOutput = "set-output"; 33 | 34 | private static readonly Lazy s_all = new(() => 35 | [ 36 | SetEnv, 37 | AddMask, 38 | AddPath, 39 | Echo, 40 | Debug, 41 | Error, 42 | Warning, 43 | Notice, 44 | Group, 45 | EndGroup, 46 | SaveState, 47 | SetOutput 48 | ]); 49 | 50 | internal static bool IsConventional(string? command) 51 | { 52 | return s_all.Value.Contains(command); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Actions.Core/Commands/DefaultCommandIssuer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Commands; 5 | 6 | /// 7 | internal sealed class DefaultCommandIssuer(IConsole console) : ICommandIssuer 8 | { 9 | /// 10 | public void Issue(string commandName, string? message = default) 11 | { 12 | IssueCommand(commandName, null, message); 13 | } 14 | 15 | /// 16 | public void IssueCommand( 17 | string commandName, 18 | IReadOnlyDictionary? properties = default, 19 | string? message = default) 20 | { 21 | var cmd = new Command( 22 | commandName, message, properties); 23 | 24 | if (cmd is not { Conventional: true }) 25 | { 26 | console.WriteLine("Issuing unconventional command."); 27 | } 28 | 29 | var commandMessage = cmd.ToString(); 30 | console.WriteLine(commandMessage); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Actions.Core/Commands/DefaultFileCommandIssuer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Commands; 5 | 6 | /// 7 | internal sealed class DefaultFileCommandIssuer( 8 | Func writeLineTask) : IFileCommandIssuer 9 | { 10 | private readonly Func _writeLineTask = writeLineTask.ThrowIfNull(); 11 | 12 | /// 13 | ValueTask IFileCommandIssuer.IssueFileCommandAsync( 14 | string commandSuffix, TValue message, JsonTypeInfo? typeInfo) 15 | { 16 | var filePath = GetEnvironmentVariable($"{GITHUB_}{commandSuffix}"); 17 | 18 | if (string.IsNullOrWhiteSpace(filePath)) 19 | { 20 | throw new ArgumentException( 21 | "Unable to find environment variable for file " + 22 | $"command suffix '{commandSuffix} ({GITHUB_}{commandSuffix})'."); 23 | } 24 | 25 | return File.Exists(filePath) switch 26 | { 27 | false => throw new FileNotFoundException( 28 | $"Missing file at path: '{filePath}' for file command '{commandSuffix}'."), 29 | 30 | _ => _writeLineTask.Invoke(filePath, message.ToCommandValue(typeInfo)) 31 | }; 32 | } 33 | 34 | /// 35 | string IFileCommandIssuer.PrepareKeyValueMessage( 36 | string key, TValue value, JsonTypeInfo? typeInfo) 37 | { 38 | var delimiter = $"ghadelimiter_{Guid.NewGuid()}"; 39 | var convertedValue = value.ToCommandValue(typeInfo); 40 | 41 | // These should realistically never happen, but just in case someone finds a 42 | // way to exploit guid generation let's not allow keys or values that contain 43 | // the delimiter. 44 | if (key.Contains(delimiter, StringComparison.OrdinalIgnoreCase)) 45 | { 46 | throw new InvalidOperationException( 47 | $"Unexpected input: name should not contain the delimiter {delimiter}"); 48 | } 49 | 50 | if (convertedValue.Contains(delimiter, StringComparison.OrdinalIgnoreCase)) 51 | { 52 | throw new InvalidOperationException( 53 | $"Unexpected input: value should not contain the delimiter {delimiter}"); 54 | } 55 | 56 | return $"{key}<<{delimiter}{NewLine}{convertedValue}{NewLine}{delimiter}"; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Actions.Core/Commands/ICommandIssuer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Commands; 5 | 6 | /// 7 | /// The utility used to issue commands. 8 | /// 9 | internal interface ICommandIssuer 10 | { 11 | /// 12 | /// Issue a formal command, given its , and . 13 | /// The following format is adhered to: 14 | /// ::name key=value,key=value::message 15 | /// Consider the following examples: 16 | /// 17 | /// 18 | /// ::warning::This is the message 19 | /// ::set-env name=MY_VAR::some value 20 | /// 21 | /// 22 | /// 23 | /// Formal command name as defined in 24 | /// Properties to issue as part of the command, written as key-value pairs. 25 | /// An arbitrary message value 26 | void IssueCommand( 27 | string commandName, 28 | IReadOnlyDictionary? properties = default, 29 | string? message = default); 30 | 31 | /// 32 | /// Issue a formal command, given its and . 33 | /// 34 | /// Formal command name as defined in 35 | /// An arbitrary message value 36 | void Issue(string commandName, string? message = default); 37 | } 38 | -------------------------------------------------------------------------------- /src/Actions.Core/Commands/IFileCommandIssuer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Commands; 5 | 6 | /// 7 | /// The utility used to issue file-based commands. 8 | /// 9 | internal interface IFileCommandIssuer 10 | { 11 | /// 12 | /// Asynchronous I/O that issues a command that corresponds to the 13 | /// given , with the given value. 14 | /// 15 | /// The command suffix as found in 16 | /// An arbitrary message value 17 | /// The JSON type info used to serialize. 18 | /// A task that represents the asynchronous operation of writing the message to file. 19 | ValueTask IssueFileCommandAsync( 20 | string commandSuffix, 21 | TValue message, 22 | JsonTypeInfo? typeInfo = null); 23 | 24 | /// 25 | /// Prepares a key-value message, given the and . 26 | /// 27 | /// The key used as the left-operand. 28 | /// The value used as the right-operand. 29 | /// The JSON type info used to serialize. 30 | /// 31 | /// A string representation of the key-value pair, formatted 32 | /// with the appropriate unique delimiter. 33 | /// 34 | string PrepareKeyValueMessage( 35 | string key, 36 | TValue value, 37 | JsonTypeInfo? typeInfo = null); 38 | } 39 | -------------------------------------------------------------------------------- /src/Actions.Core/EnvironmentVariables/Prefixes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.EnvironmentVariables; 5 | 6 | /// 7 | /// A collection of environment variable prefixes, used with corresponding . 8 | /// 9 | [System.Diagnostics.CodeAnalysis.SuppressMessage( 10 | "Naming", 11 | "CA1707:Identifiers should not contain underscores", 12 | Justification = "These values correspond to environment variables and I want them to match exactly.")] 13 | #if ACTIONS_CORE_ENVIRONMENTVARIABLES_PUBLIC 14 | public 15 | #else 16 | internal 17 | #endif 18 | static class Prefixes 19 | { 20 | /// 21 | /// The environment variable key prefix: GITHUB_. 22 | /// 23 | public const string GITHUB_ = nameof(GITHUB_); 24 | /// 25 | /// The environment variable key prefix: INPUT_. 26 | /// 27 | public const string INPUT_ = nameof(INPUT_); 28 | /// 29 | /// The environment variable key prefix: STATE_. 30 | /// 31 | public const string STATE_ = nameof(STATE_); 32 | } 33 | -------------------------------------------------------------------------------- /src/Actions.Core/EnvironmentVariables/Suffixes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.EnvironmentVariables; 5 | 6 | /// 7 | /// A collection of environment variable suffixes, used with corresponding . 8 | /// 9 | #if ACTIONS_CORE_ENVIRONMENTVARIABLES_PUBLIC 10 | public 11 | #else 12 | internal 13 | #endif 14 | static class Suffixes 15 | { 16 | /// 17 | /// The environment variable key suffix: ENV. 18 | /// 19 | public const string ENV = nameof(ENV); 20 | /// 21 | /// The environment variable key suffix: STATE. 22 | /// 23 | public const string STATE = nameof(STATE); 24 | /// 25 | /// The environment variable key suffix: OUTPUT. 26 | /// 27 | public const string OUTPUT = nameof(OUTPUT); 28 | } 29 | -------------------------------------------------------------------------------- /src/Actions.Core/ExitCode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core; 5 | 6 | /// 7 | /// The code to exit an action 8 | /// 9 | public enum ExitCode 10 | { 11 | /// 12 | /// A code indicating that the action was successful 13 | /// 14 | Success = 0, 15 | 16 | /// 17 | /// A code indicating that the action was a failure 18 | /// 19 | Failure = 1 20 | }; 21 | -------------------------------------------------------------------------------- /src/Actions.Core/Extensions/ArgumentNullExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Extensions; 5 | 6 | internal static class ArgumentNullExceptionExtensions 7 | { 8 | /// 9 | /// Throws an if is . 10 | /// 11 | /// 12 | /// The reference type argument to validate as non-null. 13 | /// The name of the parameter with which argument corresponds. If you omit this parameter, 14 | /// the name of argument is used. 15 | /// The value of the argument if it is not . 16 | internal static T ThrowIfNull( 17 | this T? argument, 18 | [CallerArgumentExpression(nameof(argument))] string? paramName = null) 19 | { 20 | ArgumentNullException.ThrowIfNull(argument, paramName); 21 | 22 | return argument; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Actions.Core/Extensions/GenericExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Extensions; 5 | 6 | internal static class GenericExtensions 7 | { 8 | /// 9 | /// Converts the specified object as: 10 | /// 11 | /// An empty string, when . 12 | /// The value of the string, when is a string type. 13 | /// A JSON string, when the is an object. 14 | /// 15 | /// 16 | /// The generic-type parameter of the given . 17 | /// The in context. 18 | /// The JSON type info, used to serialize the type. 19 | /// The string representation of the . 20 | internal static string ToCommandValue(this T? value, JsonTypeInfo? typeInfo = null) 21 | { 22 | return IsAnonymousType(typeof(T)) 23 | ? throw new ArgumentException("Generic type T, cannot be anonymous type!") 24 | : value switch 25 | { 26 | null => string.Empty, 27 | string @string => @string, 28 | bool @bool => @bool ? "true" : "false", 29 | _ when typeInfo is null => value?.ToString() ?? string.Empty, 30 | _ => JsonSerializer.Serialize(value, typeInfo) 31 | }; 32 | } 33 | 34 | // From: https://stackoverflow.com/a/1650965/2410379 35 | internal static bool IsAnonymousType(this Type type) 36 | { 37 | var typeName = type.Name; 38 | 39 | return typeName.Length switch 40 | { 41 | < 3 => false, 42 | _ => typeName[0] is '<' 43 | && typeName[1] is '>' 44 | && typeName.IndexOf("AnonymousType", StringComparison.Ordinal) > 0 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Actions.Core/Extensions/ObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Extensions; 5 | 6 | internal static class ObjectExtensions 7 | { 8 | /// 9 | /// Converts the given to a self-identifying dictionary, 10 | /// where the key name is the property name with itself as a value. 11 | /// 12 | /// The generic-type parameter of the given . 13 | /// The in context. 14 | /// The name of the parameter with which argument corresponds. If you omit this parameter, 15 | /// the name of argument is used. 16 | /// A new dictionary, with a single key-value pair. 17 | internal static Dictionary ToCommandProperties( 18 | this T value, 19 | [CallerArgumentExpression(nameof(value))] string? paramName = null) 20 | { 21 | return new() 22 | { 23 | [paramName!] = value?.ToString() ?? string.Empty 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Actions.Core/Extensions/ProcessExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Extensions; 5 | 6 | internal static class ProcessExtensions 7 | { 8 | internal static void GracefulOrForcedShutdown(this int pid) 9 | { 10 | Process.GetProcessById(pid) 11 | ?.GracefulOrForcedShutdown(); 12 | } 13 | 14 | internal static void GracefulOrForcedShutdown(this Process process) 15 | { 16 | if (process is null) 17 | { 18 | return; 19 | } 20 | 21 | if (process.CloseMainWindow() is false) 22 | { 23 | process.WaitForExit(TimeSpan.FromSeconds(3)); 24 | } 25 | 26 | while (process is not { HasExited: true }) 27 | { 28 | process.Kill(true); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Actions.Core/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Extensions; 5 | 6 | /// 7 | /// Extensions for registering services with the . 8 | /// 9 | public static class ServiceCollectionExtensions 10 | { 11 | /// 12 | /// Adds all the services required to interact with GitHubClientFactory Action workflows. 13 | /// Consumers should require the to interact with the workflow. 14 | /// 15 | public static IServiceCollection AddGitHubActionsCore(this IServiceCollection services) 16 | { 17 | services.AddSingleton(); 18 | services.AddTransient(); 19 | services.AddTransient( 20 | _ => new DefaultFileCommandIssuer( 21 | async (filePath, message) => 22 | { 23 | using var writer = new StreamWriter(filePath, append: true, Encoding.UTF8); 24 | await writer.WriteLineAsync(message); 25 | })); 26 | services.AddTransient(); 27 | 28 | return services; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Actions.Core/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using System.Diagnostics; 5 | global using System.Globalization; 6 | global using System.Runtime.CompilerServices; 7 | global using System.Text; 8 | global using System.Text.Json; 9 | global using System.Text.Json.Serialization.Metadata; 10 | 11 | global using Actions.Core.Commands; 12 | global using Actions.Core.Extensions; 13 | global using Actions.Core.Markdown; 14 | global using Actions.Core.Output; 15 | global using Actions.Core.Services; 16 | global using Actions.Core.Summaries; 17 | global using Actions.Core.Workflows; 18 | 19 | global using Microsoft.Extensions.DependencyInjection; 20 | 21 | global using static System.Environment; 22 | global using static System.IO.Path; 23 | 24 | global using static Actions.Core.EnvironmentVariables.Keys; 25 | global using static Actions.Core.EnvironmentVariables.Prefixes; 26 | global using static Actions.Core.EnvironmentVariables.Suffixes; 27 | -------------------------------------------------------------------------------- /src/Actions.Core/InputOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core; 5 | 6 | /// 7 | /// 8 | /// Inspired by 9 | /// 10 | /// 11 | /// Optional. Whether the input is required. If required and not present, will throw. Defaults to false. 12 | /// 13 | /// 14 | /// Optional. Whether leading/trailing whitespace will be trimmed for the input. Defaults to true. 15 | /// 16 | public readonly record struct InputOptions( 17 | bool Required = false, 18 | bool TrimWhitespace = true); -------------------------------------------------------------------------------- /src/Actions.Core/Markdown/AlertType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Markdown; 5 | 6 | /// 7 | /// The GitHub flavored markdown alert type. 8 | /// For more information, see 9 | /// 10 | public enum AlertType 11 | { 12 | /// 13 | /// Renders as a blue note alert with an icon that looks like an i in a circle. 14 | /// 15 | /// 16 | /// Highlights information that users should take into account, even when skimming. 17 | /// 18 | Note, 19 | 20 | /// 21 | /// Renders as a green tip alert with an icon that looks like a lightbulb. 22 | /// 23 | /// 24 | /// Optional information to help a user be more successful. 25 | /// 26 | Tip, 27 | 28 | /// 29 | /// Renders as a purple important alert with an icon that looks like an exclamation in an arrowed square callout. 30 | /// 31 | /// 32 | /// Crucial information necessary for users to succeed. 33 | /// 34 | Important, 35 | 36 | /// 37 | /// Renders as a orange warning alert with an icon that looks like a warning triangle. 38 | /// 39 | /// 40 | /// Critical content demanding immediate user attention due to potential risks. 41 | /// 42 | Warning, 43 | 44 | /// 45 | /// Renders as a red caution alert with an icon that looks like a stop sign. 46 | /// 47 | /// 48 | /// Negative potential consequences of an action. 49 | /// 50 | Caution 51 | }; 52 | -------------------------------------------------------------------------------- /src/Actions.Core/Markdown/TableColumnAlignment.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Markdown; 5 | 6 | /// 7 | /// The alignment of the table columns. Use for either markdown or HTML tables. 8 | /// 9 | public enum TableColumnAlignment 10 | { 11 | /// 12 | /// The default alignment for the table head. When writing markdown tables with the 13 | /// API, results in "---". 14 | /// When writing HTML tables with the 15 | /// API, results in no attributes. 16 | /// 17 | Center, 18 | 19 | /// 20 | /// The default alignment for the table head. When writing markdown tables with the 21 | /// API, results in ":--". 22 | /// When writing HTML tables with the 23 | /// API, results in align="left" attribute. 24 | /// 25 | Left, 26 | 27 | /// 28 | /// The default alignment for the table head. When writing markdown tables with the 29 | /// API, results in "--:". 30 | /// When writing HTML tables with the 31 | /// API, results in align="right" attribute. 32 | /// 33 | Right 34 | } 35 | -------------------------------------------------------------------------------- /src/Actions.Core/Markdown/TaskItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Markdown; 5 | 6 | /// 7 | /// Represents a GitHub flavored markdown task item. 8 | /// 9 | /// the content to render for the task 10 | /// (optional) whether the task is complete, default: false 11 | public record class TaskItem(string Content, bool IsComplete = false); 12 | -------------------------------------------------------------------------------- /src/Actions.Core/Output/DefaultConsole.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Output; 5 | 6 | /// 7 | internal sealed class DefaultConsole : IConsole 8 | { 9 | } -------------------------------------------------------------------------------- /src/Actions.Core/Output/IConsole.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Output; 5 | 6 | /// 7 | /// A console interface that abstracts the interactions of the . 8 | /// 9 | public interface IConsole 10 | { 11 | /// 12 | void ExitWithCode(int exitCode = 0) 13 | { 14 | Exit(Environment.ExitCode = exitCode); 15 | } 16 | 17 | /// 18 | void Write(string? message = null) 19 | { 20 | Console.Write(message); 21 | } 22 | 23 | /// 24 | void WriteLine(string? message = null) 25 | { 26 | Console.WriteLine(message); 27 | } 28 | 29 | /// 30 | void WriteError(string message) 31 | { 32 | Console.Error.Write(message); 33 | } 34 | 35 | /// 36 | void WriteErrorLine(string message) 37 | { 38 | Console.Error.WriteLine(message); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Actions.Core/README.md: -------------------------------------------------------------------------------- 1 | # `GitHub.Actions.Core` package 2 | 3 | To install the [`GitHub.Actions.Core`](https://www.nuget.org/packages/GitHub.Actions.Core) NuGet package: 4 | 5 | ```xml 6 | 7 | ``` 8 | 9 | Or use the [`dotnet add package`](https://learn.microsoft.com/dotnet/core/tools/dotnet-add-package) .NET CLI command: 10 | 11 | ```bash 12 | dotnet add package GitHub.Actions.Core 13 | ``` 14 | 15 | ## Get the `ICoreService` instance 16 | 17 | To use the `ICoreService` in your .NET project, register the services with an `IServiceCollection` instance by calling `AddGitHubActionsCore` and then your consuming code can require the `ICoreService` via constructor dependency injection. 18 | 19 | ```csharp 20 | using Microsoft.Extensions.DependencyInjection; 21 | using Actions.Core; 22 | using Actions.Core.Extensions; 23 | 24 | using var provider = new ServiceCollection() 25 | .AddGitHubActionsCore() 26 | .BuildServiceProvider(); 27 | 28 | var core = provider.GetRequiredService(); 29 | ``` 30 | 31 | ## `GitHub.Actions.Core` 32 | 33 | This was modified, but borrowed from the [_core/README.md_](https://github.com/actions/toolkit/blob/main/packages/core/README.md). 34 | 35 | > Core functions for setting results, logging, registering secrets and exporting variables across actions 36 | 37 | ### Using declarations 38 | 39 | ```csharp 40 | global using Actions.Core; 41 | ``` 42 | 43 | #### Inputs/Outputs 44 | 45 | Action inputs can be read with `GetInput` which returns a `string` or `GetBoolInput` which parses a `bool` based on the [yaml 1.2 specification](https://yaml.org/spec/1.2/spec.html#id2804923). If `required` is `false`, the input should have a default value in `action.yml`. 46 | 47 | Outputs can be set with `SetOutputAsync` which makes them available to be mapped into inputs of other actions to ensure they are decoupled. 48 | 49 | ```csharp 50 | var myInput = core.GetInput("inputName", new InputOptions(true)); 51 | var myBoolInput = core.GetBoolInput("boolInputName", new InputOptions(true)); 52 | var myMultilineInput = core.GetMultilineInput("multilineInputName", new InputOptions(true)); 53 | await core.SetOutputAsync("outputKey", "outputVal"); 54 | ``` 55 | 56 | #### Exporting variables 57 | 58 | Since each step runs in a separate process, you can use `ExportVariableAsync` to add it to this step and future steps environment blocks. 59 | 60 | ```csharp 61 | await core.ExportVariableAsync("envVar", "Val"); 62 | ``` 63 | 64 | #### Setting a secret 65 | 66 | Setting a secret registers the secret with the runner to ensure it is masked in logs. 67 | 68 | ```csharp 69 | core.SetSecret("myPassword"); 70 | ``` 71 | 72 | #### PATH manipulation 73 | 74 | To make a tool's path available in the path for the remainder of the job (without altering the machine or containers state), use `AddPathAsync`. The runner will prepend the path given to the jobs PATH. 75 | 76 | ```csharp 77 | await core.AddPathAsync("/path/to/mytool"); 78 | ``` 79 | 80 | #### Exit codes 81 | 82 | You should use this library to set the failing exit code for your action. If status is not set and the script runs to completion, that will lead to a success. 83 | 84 | ```csharp 85 | using var provider = new ServiceCollection() 86 | .AddGitHubActions() 87 | .BuildServiceProvider(); 88 | 89 | var core = provider.GetRequiredService(); 90 | 91 | try 92 | { 93 | // Do stuff 94 | } 95 | catch (Exception ex) 96 | { 97 | // SetFailed logs the message and sets a failing exit code 98 | core.SetFailed($"Action failed with error {ex}""); 99 | } 100 | ``` 101 | 102 | #### Logging 103 | 104 | Finally, this library provides some utilities for logging. Note that debug logging is hidden from the logs by default. This behavior can be toggled by enabling the [Step Debug Logs](../../docs/action-debugging.md#step-debug-logs). 105 | 106 | ```csharp 107 | using var provider = new ServiceCollection() 108 | .AddGitHubActions() 109 | .BuildServiceProvider(); 110 | 111 | var core = provider.GetRequiredService(); 112 | 113 | var myInput = core.GetInput("input"); 114 | try 115 | { 116 | core.Debug("Inside try block"); 117 | 118 | if (!myInput) 119 | { 120 | core.Warning("myInput was not set"); 121 | } 122 | 123 | if (core.IsDebug) 124 | { 125 | // curl -v https://github.com 126 | } 127 | else 128 | { 129 | // curl https://github.com 130 | } 131 | 132 | // Do stuff 133 | core.Info("Output to the actions build log"); 134 | 135 | core.Notice("This is a message that will also emit an annotation"); 136 | } 137 | catch (Exception ex) 138 | { 139 | core.Error($"Error {ex}, action may still succeed though"); 140 | } 141 | ``` 142 | 143 | This library can also wrap chunks of output in foldable groups. 144 | 145 | ```csharp 146 | using var provider = new ServiceCollection() 147 | .AddGitHubActions() 148 | .BuildServiceProvider(); 149 | 150 | var core = provider.GetRequiredService(); 151 | 152 | // Manually wrap output 153 | core.StartGroup("Do some function"); 154 | SomeFunction(); 155 | core.EndGroup(); 156 | 157 | // Wrap an asynchronous function call 158 | var result = await core.GroupAsync("Do something async", async () => 159 | { 160 | var response = await MakeHttpRequestAsync(); 161 | return response 162 | }); 163 | ``` 164 | 165 | #### Styling output 166 | 167 | Colored output is supported in the Action logs via standard [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 3/4 bit, 8 bit and 24 bit colors are all supported. 168 | 169 | Foreground colors: 170 | 171 | ```csharp 172 | // 3/4 bit 173 | core.Info("\u001b[35mThis foreground will be magenta"); 174 | 175 | // 8 bit 176 | core.Info("\u001b[38;5;6mThis foreground will be cyan"); 177 | 178 | // 24 bit 179 | core.Info("\u001b[38;2;255;0;0mThis foreground will be bright red"); 180 | ``` 181 | 182 | Background colors: 183 | 184 | ```csharp 185 | // 3/4 bit 186 | core.Info("\u001b[43mThis background will be yellow"); 187 | 188 | // 8 bit 189 | core.Info("\u001b[48;5;6mThis background will be cyan"); 190 | 191 | // 24 bit 192 | core.Info("\u001b[48;2;255;0;0mThis background will be bright red"); 193 | ``` 194 | 195 | Special styles: 196 | 197 | ```csharp 198 | core.Info("\u001b[1mBold text"); 199 | core.Info("\u001b[3mItalic text"); 200 | core.Info("\u001b[4mUnderlined text"); 201 | ``` 202 | 203 | ANSI escape codes can be combined with one another: 204 | 205 | ```csharp 206 | core.Info("\u001b[31;46mRed foreground with a cyan background and \u001b[1mbold text at the end"); 207 | ``` 208 | 209 | > [!NOTE] 210 | > Escape codes reset at the start of each line. 211 | 212 | ```csharp 213 | core.Info("\u001b[35mThis foreground will be magenta"); 214 | core.Info("This foreground will reset to the default"); 215 | ``` 216 | -------------------------------------------------------------------------------- /src/Actions.Core/Summaries/SummaryImageOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Summaries; 5 | 6 | /// 7 | /// The options for the summary image. 8 | /// 9 | /// (optional) The width of the image in pixels. Must be an integer without a unit. 10 | /// (optional) The height of the image in pixels. Must be an integer without a unit. 11 | public readonly record struct SummaryImageOptions( 12 | int? Width = null, 13 | int? Height = null); 14 | -------------------------------------------------------------------------------- /src/Actions.Core/Summaries/SummaryTable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Summaries; 5 | 6 | /// 7 | /// Represents a table in a summary. Each row must have the same number of columns. 8 | /// Only simple cells (cells with just their 9 | /// values populated) are supported. 10 | /// 11 | /// The row used for the heading 12 | /// The rows in the table. 13 | public readonly record struct SummaryTable( 14 | SummaryTableRow Heading, 15 | SummaryTableRow[] Rows); 16 | -------------------------------------------------------------------------------- /src/Actions.Core/Summaries/SummaryTableCell.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Summaries; 5 | 6 | /// 7 | /// Represents a row in a summary table. 8 | /// 9 | /// Cell content 10 | /// Render cell as header. (optional) default: false 11 | /// Number of columns the cell extends. (optional) 12 | /// Number of rows the cell extends. (optional) 13 | /// The cell align value (optional) default: unset (center) 14 | public readonly record struct SummaryTableCell( 15 | string Data, 16 | bool? Header = null, 17 | int? Colspan = null, 18 | int? Rowspan = null, 19 | TableColumnAlignment Alignment = TableColumnAlignment.Center) 20 | { 21 | /// 22 | /// Whether or not the cell is considered simple, meaning 23 | /// only the is provided. 24 | /// 25 | public bool IsSimpleCell => Header is null && Colspan is 1 && Rowspan is 1; 26 | } 27 | -------------------------------------------------------------------------------- /src/Actions.Core/Summaries/SummaryTableRow.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Summaries; 5 | 6 | /// 7 | /// Represents a row in a summary table. 8 | /// 9 | /// The cells for the row in context. 10 | public readonly record struct SummaryTableRow( 11 | SummaryTableCell[] Cells); 12 | -------------------------------------------------------------------------------- /src/Actions.Core/Summaries/SummaryWriteOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Summaries; 5 | 6 | /// 7 | /// The options for writing the summary to the file. 8 | /// 9 | /// 10 | /// (optional) Replace all existing content in summary file with buffer contents. Defaults to false. 11 | /// 12 | public readonly record struct SummaryWriteOptions(bool Overwrite = false); 13 | -------------------------------------------------------------------------------- /src/Actions.Core/Workflows/Command.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Workflows; 5 | 6 | /// 7 | /// Command format: 8 | /// ::name key=value,key=value::message 9 | /// 10 | /// 11 | /// 12 | /// ::warning::This is the message 13 | /// ::set-env name=MY_VAR::some value 14 | /// 15 | /// 16 | internal readonly record struct Command( 17 | string? CommandName = "missing.command", 18 | string? Message = default, 19 | IReadOnlyDictionary? CommandProperties = default) 20 | { 21 | private const string CMD_STRING = "::"; 22 | 23 | internal bool Conventional => 24 | CommandNames.IsConventional(CommandName); 25 | 26 | /// 27 | /// The string representation of the workflow command, i.e.; 28 | /// ::name key=value,key=value::message. 29 | /// 30 | public override string ToString() 31 | { 32 | StringBuilder builder = new($"{CMD_STRING}{CommandName}"); 33 | 34 | if (CommandProperties?.Any() ?? false) 35 | { 36 | builder.Append(' '); 37 | foreach (var (isNotFirst, key, value) 38 | in CommandProperties.Select( 39 | (kvp, index) => (index is > 0, kvp.Key, kvp.Value))) 40 | { 41 | if (isNotFirst) 42 | { 43 | builder.Append(','); 44 | } 45 | builder.Append(CultureInfo.InvariantCulture, $"{key}={EscapeProperty(value)}"); 46 | } 47 | } 48 | 49 | builder.Append(CultureInfo.InvariantCulture, $"{CMD_STRING}{EscapeData(Message)}"); 50 | 51 | return builder.ToString(); 52 | } 53 | 54 | private static string EscapeProperty(string? value) 55 | { 56 | return value.ToCommandValue() 57 | .Replace("%", "%25") 58 | .Replace("\r", "%0D") 59 | .Replace("\n", "%0A") 60 | .Replace(":", "%3A") 61 | .Replace(",", "%2C"); 62 | } 63 | 64 | private static string EscapeData(string? value) 65 | { 66 | return value.ToCommandValue() 67 | .Replace("%", "%25") 68 | .Replace("\r", "%0D") 69 | .Replace("\n", "%0A"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Actions.Glob/Actions.Glob.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(DefaultTargetFrameworks) 4 | @actions/glob 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Actions.Glob/DefaultGlobPatternResolver.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Glob; 5 | 6 | /// 7 | internal sealed class DefaultGlobPatternResolver : IGlobPatternResolver 8 | { 9 | private readonly IEnumerable _includePatterns = []; 10 | private readonly IEnumerable _excludePatterns = []; 11 | 12 | private DefaultGlobPatternResolver( 13 | IEnumerable includePatterns, 14 | IEnumerable excludePatterns) => 15 | (_includePatterns, _excludePatterns) = (includePatterns, excludePatterns); 16 | 17 | /// 18 | internal static IGlobPatternResolver Factory( 19 | IEnumerable includePatterns, 20 | IEnumerable excludePatterns) 21 | { 22 | return new DefaultGlobPatternResolver(includePatterns, excludePatterns); 23 | } 24 | 25 | /// 26 | IEnumerable IGlobPatternResolver.GetGlobFiles( 27 | string? directory) 28 | { 29 | return directory.GetGlobFiles( 30 | _includePatterns, 31 | _excludePatterns); 32 | } 33 | 34 | /// 35 | GlobResult IGlobPatternResolver.GetGlobResult( 36 | string? directory) 37 | { 38 | return directory.GetGlobResult( 39 | _includePatterns, 40 | _excludePatterns); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Actions.Glob/DefaultGlobPatternResolverBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Glob; 5 | 6 | /// 7 | internal sealed class DefaultGlobPatternResolverBuilder : IGlobPatternResolverBuilder 8 | { 9 | private readonly Lazy> _includePatterns = new(() => []); 10 | private readonly Lazy> _excludePatterns = new(() => []); 11 | 12 | /// 13 | public IGlobPatternResolver Build() 14 | { 15 | var (anyInclusions, anyExclusions) = 16 | (_includePatterns.IsValueCreated, _excludePatterns.IsValueCreated); 17 | 18 | if (!anyInclusions && !anyExclusions) 19 | { 20 | throw new ArgumentException( 21 | $""" 22 | The {nameof(IGlobPatternResolverBuilder)} must have at least one include or exclude before calling build. 23 | """); 24 | } 25 | 26 | return DefaultGlobPatternResolver.Factory( 27 | anyInclusions ? _includePatterns.Value : Enumerable.Empty(), 28 | anyExclusions ? _excludePatterns.Value : Enumerable.Empty()); 29 | } 30 | 31 | /// 32 | public IGlobPatternResolverBuilder WithInclusions(params string[] patterns) 33 | { 34 | foreach (var include in patterns) 35 | { 36 | _includePatterns.Value.Add(include); 37 | } 38 | 39 | return this; 40 | } 41 | 42 | /// 43 | public IGlobPatternResolverBuilder WithExclusions(params string[] patterns) 44 | { 45 | foreach (var exclude in patterns) 46 | { 47 | _excludePatterns.Value.Add(exclude); 48 | } 49 | 50 | return this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Actions.Glob/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Actions.Glob; 6 | #pragma warning restore IDE0130 // Namespace does not match folder structure 7 | 8 | /// 9 | /// Extensions for registering services with the . 10 | /// 11 | public static class ServiceCollectionExtensions 12 | { 13 | /// 14 | /// Adds all the services required to interact with Actions.Glob services. 15 | /// Consumers should require the 16 | /// to build an . 17 | /// 18 | public static IServiceCollection AddGitHubActionsGlob(this IServiceCollection services) 19 | { 20 | services.AddTransient(); 21 | 22 | return services; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Actions.Glob/Extensions/StringExtensions.Files.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Glob; 5 | 6 | /// 7 | /// Extensions on to support globbing. 8 | /// 9 | public static partial class StringExtensions 10 | { 11 | /// 12 | /// Gets the files for the specified directory path and include patterns. 13 | /// 14 | /// The directory path to search for files. 15 | /// If not provided, defaults to . 16 | /// The include patterns to use when searching for files. 17 | /// The exclude patterns to use when searching for files. 18 | /// An of file paths. 19 | /// is . 20 | public static IEnumerable GetGlobFiles( 21 | this string? directory, 22 | IEnumerable includePatterns, 23 | IEnumerable? excludePatterns = null) 24 | { 25 | return directory.GetGlobResult( 26 | includePatterns, excludePatterns) is { HasMatches: true } result 27 | ? result.Files.Select(file => file.FullName) 28 | : []; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Actions.Glob/Extensions/StringExtensions.Results.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Glob; 5 | 6 | public static partial class StringExtensions 7 | { 8 | /// 9 | /// Gets the for the specified directory path and patterns. 10 | /// 11 | /// The directory path to search for files. 12 | /// If not provided, defaults to . 13 | /// The include patterns to use when searching for files. 14 | /// The exclude patterns to use when searching for files. 15 | /// A instance representing the results of the glob operation. 16 | public static GlobResult GetGlobResult( 17 | this string? directory, 18 | IEnumerable includePatterns, 19 | IEnumerable? excludePatterns = null) 20 | { 21 | directory ??= Directory.GetCurrentDirectory(); 22 | 23 | var builder = new GlobOptionsBuilder() 24 | .WithBasePath(directory) 25 | .WithPatterns([.. includePatterns]); 26 | 27 | if (excludePatterns is { } ignore) 28 | { 29 | builder = builder.WithIgnorePatterns([.. ignore]); 30 | } 31 | 32 | var options = builder.Build(); 33 | 34 | return options.ExecuteEvaluation(); 35 | } 36 | 37 | /// 38 | /// Gets the for the specified directory path and patterns. 39 | /// 40 | /// The directory path to search for files. 41 | /// If not provided, defaults to . 42 | /// The include patterns to use when searching for files. 43 | /// A instance representing the results of the glob operation. 44 | public static GlobResult GetGlobResult( 45 | this string? directory, 46 | params string[] includePatterns) 47 | { 48 | return directory.GetGlobResult(includePatterns); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Actions.Glob/GlobResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Glob; 5 | 6 | /// 7 | /// Represents the glob result, indicating whether matches were found and their corresponding . 8 | /// 9 | /// The value from . 10 | /// The value from . 11 | public readonly record struct GlobResult( 12 | bool HasMatches, 13 | IEnumerable Files) 14 | { 15 | /// 16 | /// Implicitly converts the instance to a . 17 | /// 18 | /// The result instance to convert from. 19 | public static implicit operator GlobResult(GlobEvaluationResult result) => 20 | new( 21 | HasMatches: result.HasMatches, 22 | Files: result.Files?.Select(match => match) 23 | ?? []); 24 | } -------------------------------------------------------------------------------- /src/Actions.Glob/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Microsoft.Extensions.DependencyInjection; 5 | 6 | global using Pathological.Globbing.Extensions; 7 | global using Pathological.Globbing.Options; 8 | global using Pathological.Globbing.Results; -------------------------------------------------------------------------------- /src/Actions.Glob/Globber.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Glob; 5 | 6 | /// 7 | /// A globber instance, used to get glob results or interact glob files. 8 | /// 9 | public class Globber 10 | { 11 | private readonly IGlobPatternResolver _glob; 12 | 13 | private Globber( 14 | IEnumerable inclusions, 15 | IEnumerable? exclusions = null) => 16 | _glob = new DefaultGlobPatternResolverBuilder() 17 | .WithInclusions((inclusions ?? []).ToArray()) 18 | .WithExclusions((exclusions ?? []).ToArray()) 19 | .Build(); 20 | 21 | /// 22 | /// Creates a new instance. 23 | /// 24 | /// Required inclusion patterns. 25 | /// A new instance. 26 | public static Globber Create(params string[] inclusions) 27 | { 28 | return new(inclusions); 29 | } 30 | 31 | /// 32 | /// Creates a new instance. 33 | /// 34 | /// Required inclusion patterns. 35 | /// Optional exclusion patterns. 36 | /// A new instance. 37 | public static Globber Create( 38 | IEnumerable inclusions, 39 | IEnumerable? exclusions = null) 40 | { 41 | return new(inclusions, exclusions); 42 | } 43 | 44 | /// 45 | public GlobResult Glob(string? directory = null) 46 | { 47 | return _glob.GetGlobResult(directory); 48 | } 49 | 50 | /// 51 | public IEnumerable GlobFiles(string? directory = null) 52 | { 53 | return _glob.GetGlobFiles(directory); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Actions.Glob/IGlobPatternResolver.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Glob; 5 | 6 | /// 7 | /// A service that resolves glob patterns. 8 | /// 9 | public interface IGlobPatternResolver 10 | { 11 | /// 12 | /// Gets the for the the given 13 | /// that was used to create this instance. 14 | /// 15 | /// The directory path to search for files. 16 | /// If not provided, defaults to . 17 | /// A instance representing the results of the glob operation. 18 | /// is . 19 | GlobResult GetGlobResult(string? directory = null); 20 | 21 | /// 22 | /// Gets the files for the specified directory path from the given 23 | /// that was used to create this instance. 24 | /// 25 | /// The directory path to search for files. 26 | /// If not provided, defaults to . 27 | /// An of file paths. 28 | /// is . 29 | IEnumerable GetGlobFiles(string? directory = null); 30 | } -------------------------------------------------------------------------------- /src/Actions.Glob/IGlobPatternResolverBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Glob; 5 | 6 | /// 7 | /// A builder for creating an . 8 | /// Both inclusive () and exclusive () 9 | /// patterns are optionally added to the builder, and then the method is 10 | /// called to create a . 11 | /// 12 | public interface IGlobPatternResolverBuilder 13 | { 14 | /// 15 | /// A fluent method for adding inclusive patterns to the builder. 16 | /// 17 | /// Patterns to include in the . 18 | /// Itself as a fluent API with the added inclusions. 19 | IGlobPatternResolverBuilder WithInclusions(params string[] includePatterns); 20 | 21 | /// 22 | /// A fluent method for adding exclusive patterns to the builder. 23 | /// 24 | /// Patterns to exclude in the . 25 | /// Itself as a fluent API with the added exclusions. 26 | IGlobPatternResolverBuilder WithExclusions(params string[] excludePatterns); 27 | 28 | /// 29 | /// Builds the from the builder, with all inclusions and exclusions added. 30 | /// 31 | /// An instance. 32 | IGlobPatternResolver Build(); 33 | } 34 | -------------------------------------------------------------------------------- /src/Actions.Glob/README.md: -------------------------------------------------------------------------------- 1 | # `GitHub.Actions.Glob` package 2 | 3 | To install the [`GitHub.Actions.Glob`](https://www.nuget.org/packages/GitHub.Actions.Glob) NuGet package: 4 | 5 | ```xml 6 | 7 | ``` 8 | 9 | Or use the [`dotnet add package`](https://learn.microsoft.com/dotnet/core/tools/dotnet-add-package) .NET CLI command: 10 | 11 | ```bash 12 | dotnet add package GitHub.Actions.Glob 13 | ``` 14 | 15 | ### Get the `IGlobPatternResolverBuilder` instance 16 | 17 | To use the `IGlobPatternResolverBuilder` in your .NET project, register the services with an `IServiceCollection` instance by calling `AddGitHubActionsGlob` and then your consuming code can require the `IGlobPatternResolverBuilder` via constructor dependency injection. 18 | 19 | ```csharp 20 | using Microsoft.Extensions.DependencyInjection; 21 | using Actions.Glob; 22 | using Actions.Glob.Extensions; 23 | 24 | using var provider = new ServiceCollection() 25 | .AddGitHubActionsGlob() 26 | .BuildServiceProvider(); 27 | 28 | var glob = provider.GetRequiredService(); 29 | ``` 30 | 31 | ## `GitHub.Actions.Glob` 32 | 33 | This was modified, but borrowed from the [_glob/README.md_](https://github.com/actions/toolkit/blob/main/packages/glob/README.md). 34 | 35 | > You can use this package to search for files matching glob patterns. 36 | 37 | ### Basic usage 38 | 39 | Relative paths and absolute paths are both allowed. Relative paths are rooted against the current working directory. 40 | 41 | ```csharp 42 | using Actions.Glob; 43 | using Actions.Glob.Extensions; 44 | 45 | var patterns = new[] { "**/tar.gz", "**/tar.bz" }; 46 | var globber = Globber.Create(patterns); 47 | var files = globber.GlobFiles(); 48 | ``` 49 | 50 | ### Get all files recursively 51 | 52 | ```csharp 53 | using Actions.Glob; 54 | using Actions.Glob.Extensions; 55 | 56 | var globber = Globber.Create("**/*"); 57 | var files = globber.GlobFiles(); 58 | ``` 59 | 60 | ### Iterating files 61 | 62 | When dealing with a large amount of results, consider iterating the results as they are returned: 63 | 64 | ```csharp 65 | using Actions.Glob; 66 | using Actions.Glob.Extensions; 67 | 68 | var globber = Globber.Create("**/*"); 69 | foreach (var file in globber.GlobFiles()) 70 | { 71 | // Do something with the file 72 | } 73 | ``` 74 | 75 | ## Recommended action inputs 76 | 77 | When an action allows a user to specify input patterns, it is generally recommended to 78 | allow users to opt-out from following symbolic links. 79 | 80 | Snippet from `action.yml`: 81 | 82 | ```yaml 83 | inputs: 84 | files: 85 | description: "Files to print" 86 | required: true 87 | ``` 88 | 89 | And corresponding toolkit consumption: 90 | 91 | ```csharp 92 | using Microsoft.Extensions.DependencyInjection; 93 | using Actions.Core; 94 | using Actions.Core.Extensions; 95 | using Actions.Glob; 96 | using Actions.Glob.Extensions; 97 | 98 | using var provider = new ServiceCollection() 99 | .AddGitHubActionsCore() 100 | .BuildServiceProvider(); 101 | 102 | var core = provider.GetRequiredService(); 103 | 104 | var globber = Globber.Create(core.GetInput("files")) 105 | foreach (var file in globber.GlobFiles()) 106 | { 107 | // Do something with the file 108 | } 109 | ``` 110 | 111 | ## Pattern formats 112 | 113 | The patterns that are specified in the `AddExclude` and `AddInclude` methods can use the following formats to match multiple files or directories. 114 | 115 | - Exact directory or file name 116 | 117 | - `some-file.txt` 118 | - `path/to/file.txt` 119 | 120 | - Wildcards `*` in file and directory names that represent zero to many characters not including separator characters. 121 | 122 | | Value | Description | 123 | |----------------|------------------------------------------------------------------------| 124 | | `*.txt` | All files with *.txt* file extension. | 125 | | `*.*` | All files with an extension. | 126 | | `*` | All files in top-level directory. | 127 | | `.*` | File names beginning with '.'. | 128 | | `*word*` | All files with 'word' in the filename. | 129 | | `readme.*` | All files named 'readme' with any file extension. | 130 | | `styles/*.css` | All files with extension '.css' in the directory 'styles/'. | 131 | | `scripts/*/*` | All files in 'scripts/' or one level of subdirectory under 'scripts/'. | 132 | | `images*/*` | All files in a folder with name that is or begins with 'images'. | 133 | 134 | - Arbitrary directory depth (`/**/`). 135 | 136 | | Value | Description | 137 | |------------|---------------------------------------------| 138 | | `**/*` | All files in any subdirectory. | 139 | | `dir/**/*` | All files in any subdirectory under 'dir/'. | 140 | 141 | - Relative paths. 142 | 143 | To match all files in a directory named "shared" at the sibling level to the base directory, use `../shared/*`. -------------------------------------------------------------------------------- /src/Actions.HttpClient/Actions.HttpClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(DefaultTargetFrameworks) 4 | @actions/http-client 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/ClientNames.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient; 5 | 6 | internal static class ClientNames 7 | { 8 | /// "Basic" client names corresponding to username and password. 9 | internal const string Basic = nameof(Basic); 10 | 11 | /// "Pat" client names corresponding to personal access token (PAT) 12 | internal const string Pat = nameof(Pat); 13 | 14 | /// "Bearer" client names corresponding to token. 15 | internal const string Bearer = nameof(Bearer); 16 | } 17 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Clients/DefaultHttpClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Actions.HttpClient; 6 | #pragma warning restore IDE0130 // Namespace does not match folder structure 7 | 8 | internal sealed class DefaultHttpClient(NetClient client, IRequestHandler? requestHandler = null) : IHttpClient 9 | { 10 | private static async ValueTask> ProcessResponseAsync( 11 | HttpResponseMessage response, 12 | JsonTypeInfo jsonTypeInfo, 13 | CancellationToken cancellationToken = default) 14 | { 15 | TypedResponse typedResponse = new(response.StatusCode) 16 | { 17 | ResponseHttpHeaders = response.Headers 18 | }; 19 | 20 | if (response.IsSuccessStatusCode) 21 | { 22 | var result = await response.Content.ReadFromJsonAsync(jsonTypeInfo, cancellationToken); 23 | 24 | typedResponse = typedResponse with 25 | { 26 | Result = result, 27 | }; 28 | } 29 | 30 | return typedResponse; 31 | } 32 | 33 | async ValueTask IHttpClient.DeleteAsync( 34 | string requestUri, 35 | Dictionary>? additionalHeaders, 36 | CancellationToken cancellationToken) 37 | { 38 | using var request = PrepareRequest(requestUri, HttpMethod.Delete, additionalHeaders); 39 | 40 | return await client.SendAsync(request, cancellationToken); 41 | } 42 | 43 | async ValueTask> IHttpClient.GetAsync( 44 | string requestUri, 45 | JsonTypeInfo jsonTypeInfo, 46 | Dictionary>? additionalHeaders, 47 | CancellationToken cancellationToken) 48 | { 49 | using var request = PrepareRequest(requestUri, HttpMethod.Get, additionalHeaders); 50 | 51 | var response = await client.SendAsync(request, cancellationToken); 52 | 53 | return await ProcessResponseAsync(response, jsonTypeInfo, cancellationToken); 54 | } 55 | 56 | async ValueTask IHttpClient.OptionsAsync( 57 | string requestUri, 58 | Dictionary>? additionalHeaders, 59 | CancellationToken cancellationToken) 60 | { 61 | using var request = PrepareRequest(requestUri, HttpMethod.Options, additionalHeaders); 62 | 63 | return await client.SendAsync(request, cancellationToken); 64 | } 65 | 66 | ValueTask> IHttpClient.PatchAsync( 67 | string requestUri, 68 | TData data, 69 | JsonTypeInfo dataJsonTypeInfo, 70 | JsonTypeInfo resultJsonTypeInfo, 71 | Dictionary>? additionalHeaders, 72 | CancellationToken cancellationToken) 73 | { 74 | return RequestAsync(requestUri, HttpMethod.Patch, data, dataJsonTypeInfo, resultJsonTypeInfo, additionalHeaders, cancellationToken); 75 | } 76 | 77 | ValueTask> IHttpClient.PostAsync( 78 | string requestUri, 79 | TData data, 80 | JsonTypeInfo dataJsonTypeInfo, 81 | JsonTypeInfo resultJsonTypeInfo, 82 | Dictionary>? additionalHeaders, 83 | CancellationToken cancellationToken) 84 | { 85 | return RequestAsync(requestUri, HttpMethod.Post, data, dataJsonTypeInfo, resultJsonTypeInfo, additionalHeaders, cancellationToken); 86 | } 87 | 88 | ValueTask> IHttpClient.PutAsync( 89 | string requestUri, 90 | TData data, 91 | JsonTypeInfo dataJsonTypeInfo, 92 | JsonTypeInfo resultJsonTypeInfo, 93 | Dictionary>? additionalHeaders, 94 | CancellationToken cancellationToken) 95 | { 96 | return RequestAsync(requestUri, HttpMethod.Put, data, dataJsonTypeInfo, resultJsonTypeInfo, additionalHeaders, cancellationToken); 97 | } 98 | 99 | async ValueTask IHttpClient.HeadAsync( 100 | string requestUri, 101 | Dictionary>? additionalHeaders, 102 | CancellationToken cancellationToken) 103 | { 104 | using var request = PrepareRequest(requestUri, HttpMethod.Head, additionalHeaders); 105 | 106 | return await client.SendAsync(request, cancellationToken); 107 | } 108 | 109 | private async ValueTask> RequestAsync( 110 | string requestUri, 111 | HttpMethod method, 112 | TData data, 113 | JsonTypeInfo dataJsonTypeInfo, 114 | JsonTypeInfo resultJsonTypeInfo, 115 | Dictionary>? headers, 116 | CancellationToken cancellationToken) 117 | { 118 | using var request = PrepareRequest(requestUri, method, headers); 119 | 120 | using var content = GetRequestJsonContent(data, dataJsonTypeInfo); 121 | 122 | request.Content = content; 123 | 124 | using var response = await client.SendAsync(request, cancellationToken); 125 | 126 | return await ProcessResponseAsync(response, resultJsonTypeInfo, cancellationToken); 127 | } 128 | 129 | private HttpRequestMessage PrepareRequest( 130 | string requestUri, 131 | HttpMethod method, 132 | Dictionary>? headers) 133 | { 134 | var request = new HttpRequestMessage( 135 | method, 136 | requestUri); 137 | 138 | var requestHeaders = 139 | requestHandler?.PrepareRequestHeaders(headers ?? []); 140 | 141 | foreach (var (headerKey, headerValues) in requestHeaders ?? []) 142 | { 143 | request.Headers.Add(headerKey, headerValues); 144 | } 145 | 146 | return request; 147 | } 148 | 149 | private static StringContent GetRequestJsonContent( 150 | T data, 151 | JsonTypeInfo jsonTypeInfo) 152 | { 153 | var json = JsonSerializer.Serialize(data, jsonTypeInfo); 154 | 155 | return new StringContent( 156 | content: json, 157 | encoding: Encoding.UTF8, 158 | mediaType: "application/json"); 159 | } 160 | 161 | void IDisposable.Dispose() 162 | { 163 | client?.Dispose(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Extensions/HttpMethodExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Extensions; 5 | 6 | internal static class HttpMethodExtensions 7 | { 8 | internal static bool IsRetriableMethod(this HttpMethod method) 9 | { 10 | return (method.Method[0] | 0x20) switch 11 | { 12 | // Retry HTTP methods: 13 | // OPTIONS 14 | // GET 15 | // DELETE 16 | // HEAD 17 | 'o' or 'g' or 'd' or 'h' => true, 18 | 19 | _ => false 20 | }; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Actions.HttpClient/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Extensions; 5 | 6 | /// 7 | /// Extensions methods on to register HTTP client services. 8 | /// 9 | public static class ServiceCollectionExtensions 10 | { 11 | /// 12 | /// The default user-agent HTTP header value, "dotnet-github-actions-sdk". 13 | /// 14 | public const string UserAgentHeader = "dotnet-github-actions-sdk"; 15 | 16 | /// 17 | /// Adds the required HTTP client services to the . 18 | /// Exposes the for consuming services. 19 | /// 20 | /// The to add services to. 21 | /// The optional user-argent value which will be applied 22 | /// as an HTTP header on all outgoing requests. 23 | /// 24 | public static IServiceCollection AddHttpClientServices( 25 | this IServiceCollection services, 26 | string? userAgent = UserAgentHeader) 27 | { 28 | services.AddSingleton(); 29 | 30 | services.AddHttpClient(ClientNames.Basic, ConfigureClient) 31 | .AddStandardResilienceHandler(); 32 | 33 | services.AddHttpClient(ClientNames.Bearer, ConfigureClient) 34 | .AddStandardResilienceHandler(); 35 | 36 | services.AddHttpClient(ClientNames.Pat, ConfigureClient) 37 | .AddStandardResilienceHandler(); 38 | 39 | return services; 40 | 41 | void ConfigureClient(NetClient client) 42 | { 43 | if (userAgent is not null) 44 | { 45 | client.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent); 46 | } 47 | 48 | client.DefaultRequestHeaders.Accept.Add( 49 | new MediaTypeWithQualityHeaderValue("application/json")); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Extensions; 5 | 6 | internal static class StringExtensions 7 | { 8 | internal static string ToBase64(this string value) 9 | { 10 | var inArray = Encoding.UTF8.GetBytes(value); 11 | 12 | return Convert.ToBase64String(inArray); 13 | } 14 | 15 | internal static string FromBase64(this string value) 16 | { 17 | var bytes = Convert.FromBase64String(value); 18 | 19 | return Encoding.UTF8.GetString(bytes); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Factories/DefaultHttpKeyedClientFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Actions.HttpClient; 6 | #pragma warning restore IDE0130 // Namespace does not match folder structure 7 | 8 | internal sealed class DefaultHttpKeyedClientFactory( 9 | IHttpClientFactory clientFactory) : IHttpCredentialClientFactory 10 | { 11 | IHttpClient IHttpCredentialClientFactory.CreateClient() 12 | { 13 | var client = clientFactory.CreateClient(ClientNames.Basic); 14 | 15 | return new DefaultHttpClient(client); 16 | } 17 | 18 | IHttpClient IHttpCredentialClientFactory.CreateBasicClient(string username, string password) 19 | { 20 | var client = clientFactory.CreateClient(ClientNames.Basic); 21 | var requestHandler = new BasicCredentialHandler(username, password); 22 | 23 | return new DefaultHttpClient(client, requestHandler); 24 | } 25 | 26 | IHttpClient IHttpCredentialClientFactory.CreateBearerTokenClient(string token) 27 | { 28 | var client = clientFactory.CreateClient(ClientNames.Bearer); 29 | var requestHandler = new BearerCredentialHandler(token); 30 | 31 | return new DefaultHttpClient(client, requestHandler); 32 | } 33 | 34 | IHttpClient IHttpCredentialClientFactory.CreatePersonalAccessTokenClient(string pat) 35 | { 36 | var client = clientFactory.CreateClient(ClientNames.Pat); 37 | var requestHandler = new PersonalAccessTokenHandler(pat); 38 | 39 | return new DefaultHttpClient(client, requestHandler); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Factories/IHttpCredentialClientFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Actions.HttpClient; 6 | #pragma warning restore IDE0130 // Namespace does not match folder structure 7 | 8 | /// 9 | /// Represents a factory for creating instances, given a set of credentials. 10 | /// 11 | public interface IHttpCredentialClientFactory 12 | { 13 | /// 14 | /// Creates a new instance, without any credentials. 15 | /// 16 | /// A new instance. 17 | IHttpClient CreateClient(); 18 | 19 | /// 20 | /// Creates a new instance using the provided and . 21 | /// 22 | /// The username to use for the credentials. 23 | /// The password to use for the credentials. 24 | /// A new instance with the configured HTTP Authorization header. 25 | IHttpClient CreateBasicClient(string username, string password); 26 | 27 | /// 28 | /// Creates a new instance using the provided bearer . 29 | /// 30 | /// The bearer token used as the credentials. 31 | /// A new instance with the configured HTTP Authorization header. 32 | IHttpClient CreateBearerTokenClient(string token); 33 | 34 | /// 35 | /// Creates a new instance using the provided . 36 | /// 37 | /// The personal access token (PAT) used as the credentials. 38 | /// A new instance with the configured HTTP Authorization header. 39 | IHttpClient CreatePersonalAccessTokenClient(string pat); 40 | } 41 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using System.Net; 5 | global using System.Net.Http.Headers; 6 | global using System.Net.Http.Json; 7 | global using System.Text; 8 | global using System.Text.Json; 9 | global using System.Text.Json.Serialization.Metadata; 10 | 11 | global using Actions.HttpClient.Extensions; 12 | global using Actions.HttpClient.Handlers; 13 | 14 | global using Microsoft.Extensions.DependencyInjection; 15 | 16 | global using NetClient = System.Net.Http.HttpClient; 17 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Handlers/BasicCredentialHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Handlers; 5 | 6 | internal sealed class BasicCredentialHandler(string username, string password) : IRequestHandler 7 | { 8 | Dictionary> IRequestHandler.PrepareRequestHeaders( 9 | Dictionary> headers) 10 | { 11 | ArgumentNullException.ThrowIfNull(headers); 12 | 13 | headers["Authorization"] = 14 | [ 15 | new AuthenticationHeaderValue( 16 | "Basic", 17 | $"{username}:{password}".ToBase64() 18 | ) 19 | .ToString() 20 | ]; 21 | 22 | return headers; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Handlers/BearerCredentialHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Handlers; 5 | 6 | internal sealed class BearerCredentialHandler(string token) : IRequestHandler 7 | { 8 | Dictionary> IRequestHandler.PrepareRequestHeaders( 9 | Dictionary> headers) 10 | { 11 | ArgumentNullException.ThrowIfNull(headers); 12 | 13 | headers["Authorization"] = 14 | [ 15 | new AuthenticationHeaderValue("Bearer", token).ToString() 16 | ]; 17 | 18 | return headers; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Handlers/IRequestHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Handlers; 5 | 6 | internal interface IRequestHandler 7 | { 8 | Dictionary> PrepareRequestHeaders(Dictionary> headers); 9 | } 10 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Handlers/PersonalAccessTokenHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Handlers; 5 | 6 | internal sealed class PersonalAccessTokenHandler(string pat) : IRequestHandler 7 | { 8 | Dictionary> IRequestHandler.PrepareRequestHeaders( 9 | Dictionary> headers) 10 | { 11 | ArgumentNullException.ThrowIfNull(headers); 12 | 13 | headers["Authorization"] = 14 | [ 15 | new AuthenticationHeaderValue( 16 | "Basic", 17 | $"PAT:{pat}".ToBase64() 18 | ) 19 | .ToString() 20 | ]; 21 | 22 | return headers; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/Proxy.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient; 5 | 6 | internal sealed class Proxy 7 | { 8 | public static Uri? GetProxyUrl(Uri requestUrl) 9 | { 10 | var usingSsl = requestUrl.Scheme is "https"; 11 | 12 | if (CheckBypass(requestUrl)) 13 | { 14 | return null; 15 | } 16 | 17 | var proxyVar = GetProxyVariable(usingSsl); 18 | 19 | static string? GetProxyVariable(bool usingSsl) 20 | { 21 | return usingSsl 22 | ? Environment.GetEnvironmentVariable("https_proxy") 23 | ?? Environment.GetEnvironmentVariable("HTTPS_PROXY") 24 | : Environment.GetEnvironmentVariable("http_proxy") 25 | ?? Environment.GetEnvironmentVariable("HTTP_PROXY"); 26 | } 27 | 28 | if (proxyVar is not null) 29 | { 30 | try 31 | { 32 | return new Uri(proxyVar); 33 | } 34 | catch 35 | { 36 | if (proxyVar.StartsWith("http://", StringComparison.OrdinalIgnoreCase) is false && 37 | proxyVar.StartsWith("https://", StringComparison.OrdinalIgnoreCase) is false) 38 | { 39 | return new Uri($"http://{proxyVar}"); 40 | } 41 | } 42 | } 43 | 44 | return null; 45 | } 46 | 47 | internal static bool CheckBypass(Uri requestUrl) 48 | { 49 | var hostName = requestUrl.Host; 50 | 51 | if (hostName is null) 52 | { 53 | return false; 54 | } 55 | 56 | if (requestUrl.IsLoopback) 57 | { 58 | return true; 59 | } 60 | 61 | var noProxy = Environment.GetEnvironmentVariable("no_proxy") 62 | ?? Environment.GetEnvironmentVariable("NO_PROXY"); 63 | 64 | if (noProxy is null) 65 | { 66 | return false; 67 | } 68 | 69 | string[] upperReqHosts = 70 | [ 71 | requestUrl.Host.ToUpperInvariant(), 72 | $"{requestUrl.Host.ToUpperInvariant()}:{requestUrl.Port}" 73 | ]; 74 | 75 | var upperNoProxyItems = noProxy.Split(',', StringSplitOptions.RemoveEmptyEntries) 76 | .Select(x => x.Trim().ToUpperInvariant()) 77 | .Where(x => !string.IsNullOrEmpty(x)); 78 | 79 | foreach (var upperNoProxyItem in upperNoProxyItems) 80 | { 81 | if (upperNoProxyItem is "*" || 82 | upperReqHosts.Any(x => x == upperNoProxyItem || 83 | x.EndsWith($".{upperNoProxyItem}", StringComparison.OrdinalIgnoreCase) || 84 | (upperNoProxyItem.StartsWith('.') && x.EndsWith(upperNoProxyItem, StringComparison.OrdinalIgnoreCase)))) 85 | { 86 | return true; 87 | } 88 | } 89 | 90 | return false; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/README.md: -------------------------------------------------------------------------------- 1 | # `GitHub.Actions.HttpClient` package 2 | 3 | To install the [`GitHub.Actions.HttpClient`](https://www.nuget.org/packages/GitHub.Actions.HttpClient) NuGet package: 4 | 5 | ```xml 6 | 7 | ``` 8 | 9 | Or use the [`dotnet add package`](https://learn.microsoft.com/dotnet/core/tools/dotnet-add-package) .NET CLI command: 10 | 11 | ```bash 12 | dotnet add package GitHub.Actions.HttpClient 13 | ``` 14 | 15 | ## Get started 16 | 17 | After installing the package, you can use the `IHttpClient` class to make HTTP requests: 18 | 19 | ```csharp 20 | using Actions.HttpClient; 21 | using Actions.HttpClient.Extensions; 22 | using System.Text.Json.Serialization; 23 | using Microsoft.Extensions.DependencyInjection; 24 | 25 | // Register services 26 | var provider = new ServiceCollection() 27 | .AddHttpClientServices() 28 | .BuildServiceProvider(); 29 | 30 | // Get service from provider 31 | var factory = provider.GetRequiredService(); 32 | 33 | // Create HTTP client from factory. 34 | using IHttpClient client = factory.CreateClient(); 35 | 36 | // Make request 37 | TypedResponse response = await client.GetAsync( 38 | "https://jsonplaceholder.typicode.com/todos?userId=1&completed=false", 39 | Context.Default.TodoArray); 40 | 41 | Console.WriteLine($"Status code: {response.StatusCode}"); 42 | Console.WriteLine($"Todo count: {response.Result.Length}"); 43 | 44 | public sealed record class Todo( 45 | int? UserId = null, 46 | int? Id = null, 47 | string? Title = null, 48 | bool? Completed = null); 49 | 50 | [JsonSerializable(typeof(Todo[]))] 51 | public sealed partial class Context : JsonSerializerContext { } 52 | ``` 53 | 54 | In this contrived example, you use the `IHttpClient` interface to make a GET request to the [JSONPlaceholder](https://jsonplaceholder.typicode.com/) API. You use the `TypedResponse` class to deserialize the response into a `Todo` record. 55 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/RequestOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient; 5 | 6 | /// 7 | /// Represents the options for an HTTP request. 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | public sealed record class RequestOptions( 21 | HttpRequestHeaders? Headers, 22 | int? SocketTimeout, 23 | bool? IgnoreSslError, 24 | bool? AllowRedirects, 25 | bool? AllowRedirectDowngrade, 26 | int? MaxRedirects, 27 | int? MaxSockets, 28 | bool? KeepAlive, 29 | bool? DeserializeDates, 30 | // Allows retries only on Read operations (since writes may not be idempotent) 31 | bool? AllowRetries, 32 | int? MaxRetries); 33 | -------------------------------------------------------------------------------- /src/Actions.HttpClient/TypedResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient; 5 | 6 | /// 7 | /// A strongly-typed response object that includes the HTTP status code, the result, and the response HTTP headers. 8 | /// 9 | /// The type of the result. 10 | /// The HTTP status code of the response. 11 | /// The resulting object instance for the response. 12 | /// The response HTTP headers. 13 | public sealed record class TypedResponse( 14 | HttpStatusCode StatusCode, 15 | TResult? Result = default, 16 | HttpResponseHeaders? ResponseHttpHeaders = null); 17 | -------------------------------------------------------------------------------- /src/Actions.IO/Actions.IO.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(DefaultTargetFrameworks) 4 | @actions/io 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Actions.IO/CopyOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.IO; 5 | 6 | /// 7 | /// Options object for copy cp. 8 | /// 9 | /// 10 | /// Whether to recursively copy all subdirectories. 11 | /// Defaults to false. 12 | /// 13 | /// 14 | /// Whether to overwrite existing files in the destination. 15 | /// Defaults to true. 16 | /// 17 | /// 18 | /// Whether to copy the source directory along with all the files. 19 | /// Only takes effect when recursive = true and copying a directory. 20 | /// Defaults to true. 21 | /// 22 | public readonly record struct CopyOptions( 23 | bool Recursive = false, 24 | bool Force = true, 25 | bool CopySourceDirectory = true) 26 | { 27 | /// 28 | /// Creates a new instance of . 29 | /// 30 | public CopyOptions() : this(false, true, true) { } 31 | } 32 | -------------------------------------------------------------------------------- /src/Actions.IO/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using System.Runtime.InteropServices; -------------------------------------------------------------------------------- /src/Actions.IO/IOperations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.IO; 5 | 6 | /// 7 | /// An abstraction of the available various I/O-based operations available. 8 | /// 9 | public interface IOperations 10 | { 11 | /// 12 | /// Copies a file or folder. 13 | /// 14 | /// The source path. 15 | /// The destination path. 16 | /// (Optional) copy options. 17 | void Copy(string source, string destination, CopyOptions? options = default); 18 | 19 | /// 20 | /// Moves a path. 21 | /// 22 | /// The source path. 23 | /// The destination path. 24 | /// (Optional) move options. 25 | void Move(string source, string destination, MoveOptions? options = default) 26 | { 27 | var overwrite = options?.Force ?? true; 28 | 29 | if (Utilities.IsDirectory(source) && Utilities.IsDirectory(destination) && overwrite) 30 | { 31 | Directory.Move(source, destination); 32 | } 33 | else 34 | { 35 | File.Move(source, destination, overwrite); 36 | } 37 | } 38 | 39 | /// 40 | /// Removes a path recursively with force 41 | /// 42 | /// Path to remove. 43 | void Remove(string path) 44 | { 45 | if (Utilities.IsDirectory(path)) 46 | { 47 | Directory.Delete(path, true); 48 | } 49 | else 50 | { 51 | File.Delete(path); 52 | } 53 | } 54 | 55 | /// 56 | /// Make a directory. Creates the full path with folders in between. 57 | /// 58 | /// Path to create. 59 | void MakeDirectory(string path) 60 | { 61 | Directory.CreateDirectory(path); 62 | } 63 | 64 | /// 65 | /// Returns path of a tool had the tool actually been invoked. Resolves via PATH. 66 | /// 67 | /// Name of the tool. 68 | /// The path to the tool. 69 | string Which(string tool) 70 | { 71 | if (Utilities.IsRooted(tool) && Utilities.Exists(tool)) 72 | { 73 | return tool; 74 | } 75 | 76 | var matches = FileInPath(tool); 77 | return matches?.FirstOrDefault() ?? string.Empty; 78 | } 79 | 80 | /// 81 | /// Returns a list of all occurrences of the given tool via PATH. 82 | /// 83 | /// Name of the tool. 84 | /// All occurrences of the given tool. 85 | string[] FileInPath(string tool); 86 | } 87 | -------------------------------------------------------------------------------- /src/Actions.IO/MoveOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.IO; 5 | 6 | /// 7 | /// Options object for move mv. 8 | /// 9 | /// 10 | /// Whether to overwrite existing files in the destination. 11 | /// Defaults to true. 12 | /// 13 | public readonly record struct MoveOptions( 14 | bool Force = true) 15 | { 16 | /// 17 | /// Creates a new instance of . 18 | /// 19 | public MoveOptions() : this(true) { } 20 | } 21 | -------------------------------------------------------------------------------- /src/Actions.IO/Operations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.IO; 5 | 6 | /// 7 | internal sealed class Operations : IOperations 8 | { 9 | /// 10 | public void Copy(string sourcePath, string destinationPath, CopyOptions? options = default) 11 | { 12 | CopyAll(sourcePath, destinationPath, options); 13 | } 14 | 15 | private static void CopyAll(string sourcePath, string destinationPath, CopyOptions? options = default) 16 | { 17 | var (recursive, force, copySourceDirectory) = (options ??= new(false)); 18 | 19 | if (File.Exists(sourcePath)) 20 | { 21 | if (File.Exists(destinationPath) is false || force) 22 | { 23 | File.Copy(sourcePath, destinationPath, true); 24 | } 25 | } 26 | else if (Directory.Exists(sourcePath)) 27 | { 28 | if (recursive is false) 29 | { 30 | throw new InvalidOperationException( 31 | $"Failed to copy. " + 32 | $"{sourcePath} is a directory, but tried to copy without recursive flag."); 33 | } 34 | 35 | var source = new DirectoryInfo(sourcePath); 36 | var destination = new DirectoryInfo(destinationPath); 37 | destination = copySourceDirectory 38 | ? new(Path.Combine(destinationPath, Path.GetFileName(sourcePath)!)) 39 | : destination; 40 | 41 | if (destination.Exists is false) 42 | { 43 | destination.Create(); 44 | } 45 | 46 | foreach (var dir in source.GetDirectories()) 47 | { 48 | var subdir = destination.CreateSubdirectory(dir.Name); 49 | CopyAll(dir.FullName, subdir.FullName, options); 50 | } 51 | 52 | foreach (var file in source.GetFiles()) 53 | { 54 | file.CopyTo(Path.Combine(destination.FullName, file.Name), force); 55 | } 56 | } 57 | } 58 | 59 | /// 60 | public string[] FileInPath(string tool) 61 | { 62 | ArgumentException.ThrowIfNullOrEmpty(tool); 63 | 64 | var extensions = Utilities.PathExtensions.Value; 65 | if (Utilities.IsRooted(tool)) 66 | { 67 | var filePath = Utilities.TryGetExecutablePath(tool, extensions); 68 | return filePath is { Length: > 0 } ? [filePath] : []; 69 | } 70 | 71 | if (tool.Contains(Path.PathSeparator)) 72 | { 73 | return []; 74 | } 75 | 76 | var paths = Utilities.Paths.Value; 77 | var matches = new List(); 78 | for (var i = 0; i < paths.Length; ++i) 79 | { 80 | var path = paths[i]; 81 | var filePath = Utilities.TryGetExecutablePath(Path.Combine(path, tool), extensions); 82 | if (filePath is { Length: > 0 }) 83 | { 84 | matches.Add(filePath); 85 | } 86 | } 87 | 88 | return [.. matches]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Actions.IO/README.md: -------------------------------------------------------------------------------- 1 | # `GitHub.Actions.IO` package 2 | 3 | To install the [`GitHub.Actions.IO`](https://www.nuget.org/packages/GitHub.Actions.IO) NuGet package: 4 | 5 | ```xml 6 | 7 | ``` 8 | 9 | Or use the [`dotnet add package`](https://learn.microsoft.com/dotnet/core/tools/dotnet-add-package) .NET CLI command: 10 | 11 | ```bash 12 | dotnet add package GitHub.Actions.IO 13 | ``` 14 | 15 | ## `GitHub.Actions.IO` 16 | 17 | This was modified, but borrowed from the [_glob/README.md_](https://github.com/actions/toolkit/blob/main/packages/io/README.md). 18 | 19 | > Core functions for cli filesystem scenarios 20 | 21 | ## Usage 22 | 23 | #### mkdir -p 24 | 25 | Recursively make a directory. Follows rules specified in [man mkdir](https://linux.die.net/man/1/mkdir) with the `-p` option specified: 26 | 27 | ```csharp 28 | using Action.IO; 29 | 30 | await io.mkdirP("path/to/make"); 31 | ``` 32 | 33 | #### cp/mv 34 | 35 | Copy or move files or folders. Follows rules specified in [man cp](https://linux.die.net/man/1/cp) and [man mv](https://linux.die.net/man/1/mv): 36 | 37 | ```csharp 38 | const io = require("@actions/io"); 39 | 40 | // Recursive must be true for directories 41 | const options = { recursive: true, force: false } 42 | 43 | await io.cp("path/to/directory", "path/to/dest", options); 44 | await io.mv("path/to/file", "path/to/dest"); 45 | ``` 46 | 47 | #### rm -rf 48 | 49 | Remove a file or folder recursively. Follows rules specified in [man rm](https://linux.die.net/man/1/rm) with the `-r` and `-f` rules specified. 50 | 51 | ```csharp 52 | const io = require("@actions/io"); 53 | 54 | await io.rmRF("path/to/directory"); 55 | await io.rmRF("path/to/file"); 56 | ``` 57 | 58 | #### which 59 | 60 | Get the path to a tool and resolves via paths. Follows the rules specified in [man which](https://linux.die.net/man/1/which). 61 | 62 | ```csharp 63 | using Action.IO; 64 | 65 | const exec = require("@actions/exec"); 66 | const io = require("@actions/io"); 67 | 68 | const pythonPath: string = await io.which("python", true) 69 | 70 | await exec.exec(`"${pythonPath}"`, ["main.py"]); 71 | ``` -------------------------------------------------------------------------------- /src/Actions.IO/Utilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace Actions.IO; 7 | 8 | /// 9 | /// A collection of utilities for working with file system paths. 10 | /// 11 | public static class Utilities 12 | { 13 | /// 14 | /// Gets a value indicating whether the current operating system is Windows. 15 | /// 16 | public static bool IsWindows { get; } = 17 | RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 18 | 19 | /// 20 | /// Gets all of the PATH environment variables as a array. 21 | /// 22 | public static Lazy Paths { get; } = new(() => 23 | { 24 | var path = Environment.GetEnvironmentVariable("PATH"); 25 | return path?.Split(Path.PathSeparator) ?? []; 26 | }); 27 | 28 | /// 29 | /// Gets all of the PATHEXT environment variables as a array. 30 | /// 31 | public static Lazy PathExtensions { get; } = new(() => 32 | { 33 | var pathExts = Environment.GetEnvironmentVariable("PATHEXT"); 34 | return pathExts?.Split(Path.PathSeparator) ?? []; 35 | }); 36 | 37 | /// 38 | public static bool Exists(string? path) 39 | { 40 | return File.Exists(path); 41 | } 42 | 43 | /// 44 | public static bool IsRooted(string? path) 45 | { 46 | return Path.IsPathRooted(path); 47 | } 48 | 49 | /// 50 | /// Indicates whether the given resolves as a directory. 51 | /// 52 | /// The path to evaluate. 53 | /// when refers to a 54 | /// directory, else . 55 | public static bool IsDirectory( 56 | [NotNullWhen(true)] string? path) 57 | { 58 | return Directory.Exists(path) && (File.GetAttributes(path!) & FileAttributes.Directory) is FileAttributes.Directory; 59 | } 60 | 61 | /// 62 | /// Best effort attempt to determine whether a file exists and is executable. 63 | /// 64 | /// The file path to check 65 | /// Additional file extensions to try 66 | /// If file exists and is executable, returns the file path. 67 | /// Otherwise empty string 68 | public static string TryGetExecutablePath(string filePath, string[] extensions) 69 | { 70 | if (Exists(filePath)) 71 | { 72 | return filePath; 73 | } 74 | 75 | var dirPath = Path.GetDirectoryName(filePath); 76 | if (dirPath is null) 77 | { 78 | return string.Empty; 79 | } 80 | 81 | var fileName = Path.GetFileNameWithoutExtension(filePath); 82 | foreach (var ext in extensions) 83 | { 84 | var fullPath = Path.Combine(dirPath, $"{fileName}{ext}"); 85 | if (File.Exists(fullPath)) 86 | { 87 | return fullPath; 88 | } 89 | } 90 | 91 | return string.Empty; 92 | } 93 | 94 | #pragma warning disable IDE0051 // Remove unused private members 95 | private static bool IsUnixExecutable(FileInfo file) 96 | #pragma warning restore IDE0051 // Remove unused private members 97 | { 98 | return file.Exists && (file.Attributes & FileAttributes.Archive) is FileAttributes.Archive; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Actions.Octokit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(DefaultTargetFrameworks) 4 | @actions/github 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | EnvironmentVariables 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Common/Issue.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Common; 5 | 6 | /// 7 | /// Represents a GitHub issue. 8 | /// 9 | /// The owner of the issue. The first segment of this URL: dotnet/runtime, the owner would be dotnet. 10 | /// The repo of the issue. The second segment of this URL: dotnet/runtime, the owner would be runtime. 11 | /// The issue number. 12 | public readonly record struct Issue( 13 | string Owner, 14 | string Repo, 15 | long Number); 16 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Common/Repository.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Common; 5 | 6 | /// 7 | /// Represents a GitHub repository. 8 | /// 9 | /// The owner of the issue. The first segment of this URL: dotnet/runtime, the owner would be dotnet. 10 | /// The repo of the issue. The second segment of this URL: dotnet/runtime, the owner would be runtime. 11 | public readonly record struct Repository( 12 | string Owner, 13 | string Repo); 14 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Context.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Issue = Actions.Octokit.Common.Issue; 5 | using Repository = Actions.Octokit.Common.Repository; 6 | 7 | namespace Actions.Octokit; 8 | 9 | /// 10 | /// Provides access to the GitHub Actions context. 11 | /// 12 | public sealed class Context 13 | { 14 | private static readonly Lazy s_context = new(() => new()); 15 | 16 | /// 17 | /// Gets the current GitHub Actions context. 18 | /// 19 | public static Context Current => s_context.Value; 20 | 21 | /// 22 | /// Gets webhook payload in context. 23 | /// 24 | public WebhookPayload? Payload { get; } 25 | 26 | /// 27 | /// Gets event name in context. 28 | /// 29 | public string? EventName { get; } 30 | 31 | /// 32 | /// Gets the SHA in context. 33 | /// 34 | public string? Sha { get; } 35 | 36 | /// 37 | /// Gets the REF in context. 38 | /// 39 | public string? Ref { get; } 40 | 41 | /// 42 | /// Gets the workflow in context. 43 | /// 44 | public string? Workflow { get; } 45 | 46 | /// 47 | /// Gets the action in context. 48 | /// 49 | public string? Action { get; } 50 | 51 | /// 52 | /// Gets the actor in context. 53 | /// 54 | public string? Actor { get; } 55 | 56 | /// 57 | /// Gets the job in context. 58 | /// 59 | public string? Job { get; } 60 | 61 | /// 62 | /// Gets the run attempt number in context. 63 | /// 64 | public long RunAttempt { get; } 65 | 66 | /// 67 | /// Gets the run number in context. 68 | /// 69 | public long RunNumber { get; } 70 | 71 | /// 72 | /// Gets the run id in context. 73 | /// 74 | public long RunId { get; } 75 | 76 | /// 77 | /// Gets the API URL in context. 78 | /// 79 | public string ApiUrl { get; } 80 | 81 | /// 82 | /// Gets the server URL in context. 83 | /// 84 | public string ServerUrl { get; } 85 | 86 | /// 87 | /// Gets the GraphQL URL in context. 88 | /// 89 | public string GraphQlUrl { get; } 90 | 91 | /// 92 | /// Gets the repo in context. 93 | /// 94 | public Repository Repo { get; } 95 | 96 | /// 97 | /// Gets the issue in context. 98 | /// 99 | public Issue Issue { get; } 100 | 101 | /// 102 | /// For testing purposes only! 103 | /// The , , and 104 | /// are the only objects that are 105 | /// deserialized when using this approach. All other members are 106 | /// their value. 107 | /// 108 | public static Context? FromJson( 109 | [StringSyntax(StringSyntaxAttribute.Json)] string? json) 110 | { 111 | return json switch 112 | { 113 | null or { Length: 0 } => null, 114 | 115 | _ => JsonSerializer.Deserialize( 116 | json, 117 | SourceGenerationContexts.Default.Context) 118 | }; 119 | } 120 | 121 | [JsonConstructor] 122 | internal Context(WebhookPayload? payload) 123 | { 124 | ApiUrl = "https://api.github.com"; 125 | ServerUrl = "https://github.com"; 126 | GraphQlUrl = "https://api.github.com/graphql"; 127 | 128 | Payload = payload; 129 | 130 | if (Payload is { Repository: { } }) 131 | { 132 | Issue = new( 133 | Payload.Repository.Owner.Login, 134 | Payload.Repository.Name, 135 | Payload.Issue?.Number ?? 136 | Payload.PullRequest?.Number ?? 0); 137 | 138 | Repo = new Repository( 139 | Payload.Repository.Owner.Login, 140 | Payload.Repository.Name); 141 | } 142 | } 143 | 144 | private Context() 145 | { 146 | var eventPath = GetEnvironmentVariable(GITHUB_EVENT_PATH); 147 | if (!string.IsNullOrWhiteSpace(eventPath) && File.Exists(eventPath)) 148 | { 149 | var json = File.ReadAllText(eventPath, Encoding.UTF8); 150 | 151 | Payload = JsonSerializer.Deserialize( 152 | json, SourceGenerationContexts.Default.WebhookPayload)!; 153 | } 154 | else 155 | { 156 | Console.WriteLine($"GITHUB_EVENT_PATH ${eventPath} does not exist"); 157 | } 158 | 159 | EventName = GetEnvironmentVariable(GITHUB_EVENT_NAME); 160 | Sha = GetEnvironmentVariable(GITHUB_SHA); 161 | Ref = GetEnvironmentVariable(GITHUB_REF); 162 | Workflow = GetEnvironmentVariable(GITHUB_WORKFLOW); 163 | Action = GetEnvironmentVariable(GITHUB_ACTION); 164 | Actor = GetEnvironmentVariable(GITHUB_ACTOR); 165 | Job = GetEnvironmentVariable(GITHUB_JOB); 166 | RunAttempt = long.TryParse(GetEnvironmentVariable(GITHUB_RUN_ATTEMPT), out var attempt) ? attempt : 10; 167 | RunNumber = long.TryParse(GetEnvironmentVariable(GITHUB_RUN_NUMBER), out var number) ? number : 10; 168 | RunId = long.TryParse(GetEnvironmentVariable(GITHUB_RUN_ID), out var id) ? id : 10; 169 | ApiUrl = GetEnvironmentVariable(GITHUB_API_URL) ?? "https://api.github.com"; 170 | ServerUrl = GetEnvironmentVariable(GITHUB_SERVER_URL) ?? "https://github.com"; 171 | GraphQlUrl = GetEnvironmentVariable(GITHUB_GRAPHQL_URL) ?? "https://api.github.com/graphql"; 172 | 173 | if (Payload is { Repository: { } }) 174 | { 175 | Issue = new( 176 | Payload.Repository.Owner.Login, 177 | Payload.Repository.Name, 178 | Payload.Issue?.Number ?? 179 | Payload.PullRequest?.Number ?? 0); 180 | } 181 | 182 | var repository = GetEnvironmentVariable(GITHUB_REPOSITORY); 183 | if (!string.IsNullOrWhiteSpace(repository)) 184 | { 185 | var parts = repository.Split('/'); 186 | if (parts is { Length: 2 }) 187 | { 188 | Repo = new Repository(parts[0], parts[1]); 189 | } 190 | } 191 | else if (Payload is { Repository: { } }) 192 | { 193 | Repo = new Repository( 194 | Payload.Repository.Owner.Login, 195 | Payload.Repository.Name); 196 | } 197 | } 198 | 199 | /// 200 | /// Gets a JSON representation of the current context. 201 | /// 202 | public override string ToString() 203 | { 204 | return JsonSerializer.Serialize( 205 | this, 206 | SourceGenerationContexts.Default.Context); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Extensions; 5 | 6 | /// 7 | /// Represents a collection of extension methods on 8 | /// for adding GitHub services. 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// Adds the Octokit services to the . 14 | /// 15 | /// A instance initialized with the token. 16 | /// A as materialized from the workflows environment. 17 | /// 18 | /// 19 | /// The service collection to add services to. 20 | /// The GitHub token used to create the . 21 | /// Commonly assigned from ${{ secrets.GITHUB_TOKEN }} 22 | /// 23 | /// The same service collection, but with added services. 24 | public static IServiceCollection AddGitHubClientServices( 25 | this IServiceCollection services, 26 | string gitHubToken) 27 | { 28 | services.AddSingleton(GitHubClientFactory.Create(gitHubToken)); 29 | 30 | services.AddSingleton(Context.Current); 31 | 32 | return services; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Actions.Octokit/GitHubClientFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit; 5 | 6 | /// 7 | /// Represents a factory for creating instances. 8 | /// 9 | public static class GitHubClientFactory 10 | { 11 | /// 12 | /// Creates a new from the given . 13 | /// 14 | /// The token used to initialize the client. 15 | /// A new instance. 16 | public static GitHubClient Create(string token) 17 | { 18 | ArgumentException.ThrowIfNullOrWhiteSpace(token); 19 | 20 | var tokenProvider = new TokenProvider(token); 21 | 22 | var request = RequestAdapter.Create( 23 | new TokenAuthProvider(tokenProvider)); 24 | 25 | return new GitHubClient(request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Actions.Octokit/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using System.Diagnostics.CodeAnalysis; 5 | global using System.Text; 6 | global using System.Text.Json; 7 | global using System.Text.Json.Serialization; 8 | 9 | global using Actions.Octokit.Interfaces; 10 | global using Actions.Octokit.Serialization; 11 | 12 | global using GitHub; 13 | global using GitHub.Octokit.Client.Authentication; 14 | global using GitHub.Octokit.Client; 15 | 16 | global using Microsoft.Extensions.DependencyInjection; 17 | 18 | global using static System.Environment; 19 | global using static Actions.Core.EnvironmentVariables.Keys; 20 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Interfaces/Comment.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Interfaces; 5 | 6 | /// 7 | /// Represents a comment on a GitHub issue or pull request. 8 | /// 9 | public sealed class Comment 10 | { 11 | /// 12 | /// The unique identifier of the comment. 13 | /// 14 | [JsonPropertyName("id")] 15 | public long Id { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Interfaces/Installation.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Interfaces; 5 | 6 | /// 7 | /// Represents the installation. 8 | /// 9 | public sealed class Installation 10 | { 11 | /// 12 | /// The unique identifier of the installation. 13 | /// 14 | [JsonPropertyName("id")] 15 | public long Id { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Interfaces/Owner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Interfaces; 5 | 6 | /// 7 | /// Represents the owner of a GitHub repository. 8 | /// 9 | public sealed class Owner 10 | { 11 | /// 12 | /// The login used for by the owner. 13 | /// 14 | [JsonPropertyName("login")] 15 | public required string Login { get; set; } 16 | 17 | /// 18 | /// The name of the owner. 19 | /// 20 | [JsonPropertyName("name")] 21 | public string? Name { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Interfaces/PayloadRepository.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Interfaces; 5 | 6 | /// 7 | /// Represents the payload repository. 8 | /// 9 | public sealed class PayloadRepository 10 | { 11 | /// 12 | /// The full name of the payload repository. 13 | /// 14 | [JsonPropertyName("full_name")] 15 | public string? FullName { get; set; } 16 | 17 | /// 18 | /// The name of the payload repository. 19 | /// 20 | [JsonPropertyName("name")] 21 | public required string Name { get; set; } 22 | 23 | /// 24 | /// The owner of the payload repository. 25 | /// 26 | [JsonPropertyName("owner")] 27 | public required Owner Owner { get; set; } 28 | 29 | /// 30 | /// The URL for the issues HTML of the payload repository. 31 | /// 32 | [JsonPropertyName("html_url")] 33 | public string? HtmlUrl { get; set; } 34 | } 35 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Interfaces/PullRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Interfaces; 5 | 6 | /// 7 | /// Represents a pull request. 8 | /// 9 | public sealed class PullRequest : WebhookIssue 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Interfaces/Sender.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Interfaces; 5 | 6 | /// 7 | /// Represents the sender of the GitHub event. 8 | /// 9 | public sealed class Sender 10 | { 11 | /// 12 | /// The type of event that was triggered by the sender. 13 | /// 14 | [JsonPropertyName("type")] 15 | public required string Type { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Interfaces/WebhookIssue.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Interfaces; 5 | 6 | /// 7 | /// Represents a GitHub issue. 8 | /// 9 | public class WebhookIssue 10 | { 11 | /// 12 | /// The unique identifier of the issue. 13 | /// 14 | [JsonPropertyName("number")] 15 | public long Number { get; set; } 16 | 17 | /// 18 | /// The URL for the issues HTML. 19 | /// 20 | [JsonPropertyName("html_url")] 21 | public string? HtmlUrl { get; set; } 22 | 23 | /// 24 | /// The body text of the issue. 25 | /// 26 | [JsonPropertyName("body")] 27 | public string? Body { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Interfaces/WebhookPayload.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Interfaces; 5 | 6 | /// 7 | /// The payload of a GitHub webhook. 8 | /// 9 | public sealed class WebhookPayload 10 | { 11 | /// 12 | /// The payload repository. 13 | /// 14 | [JsonPropertyName("repository")] 15 | public PayloadRepository? Repository { get; set; } 16 | 17 | /// 18 | /// The issue from the webhook payload. 19 | /// 20 | [JsonPropertyName("issue")] 21 | public WebhookIssue? Issue { get; set; } 22 | 23 | /// 24 | /// The pull request from the webhook payload. 25 | /// 26 | [JsonPropertyName("pull_request")] 27 | public PullRequest? PullRequest { get; set; } 28 | 29 | /// 30 | /// The sender of the webhook payload. 31 | /// 32 | [JsonPropertyName("sender")] 33 | public Sender? Sender { get; set; } 34 | 35 | /// 36 | /// The action from the webhook payload. 37 | /// 38 | [JsonPropertyName("action")] 39 | public string? Action { get; set; } 40 | 41 | /// 42 | /// The installation from the webhook payload. 43 | /// 44 | [JsonPropertyName("installation")] 45 | public Installation? Installation { get; set; } 46 | 47 | /// 48 | /// The comment from the webhook payload. 49 | /// 50 | [JsonPropertyName("comment")] 51 | public Comment? Comment { get; set; } 52 | 53 | /// 54 | /// Gets a JSON representation of the webhook payload. 55 | /// 56 | /// A JSON representing the webhook payload. 57 | public override string ToString() 58 | { 59 | return JsonSerializer.Serialize(this, SourceGenerationContexts.Default.WebhookPayload); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Actions.Octokit/README.md: -------------------------------------------------------------------------------- 1 | # `GitHub.Actions.Octokit` package 2 | 3 | To install the [`GitHub.Actions.Octokit`](https://www.nuget.org/packages/GitHub.Actions.Octokit) NuGet package: 4 | 5 | ```xml 6 | 7 | ``` 8 | 9 | Or use the [`dotnet add package`](https://learn.microsoft.com/dotnet/core/tools/dotnet-add-package) .NET CLI command: 10 | 11 | ```bash 12 | dotnet add package GitHub.Actions.Octokit 13 | ``` 14 | 15 | ## `GitHub.Actions.Octokit` 16 | 17 | This was modified, but borrowed from the [_glob/README.md_](https://github.com/actions/toolkit/blob/main/packages/github/README.md). 18 | 19 | > You can use this package to access a hydrated Octokit client with authentication and a set of useful defaults for GitHub Actions. 20 | 21 | ### Get the `GitHubClient` instance 22 | 23 | To use the `GitHubClient` in your .NET project, register the services with an `IServiceCollection` instance by calling `AddGitHubClientServices` and then your consuming code can require the `GitHubClient` via constructor dependency injection. 24 | 25 | ```csharp 26 | using Microsoft.Extensions.DependencyInjection; 27 | using GitHub; 28 | using Actions.Octokit; 29 | using Actions.Octokit.Extensions; 30 | 31 | using var provider = new ServiceCollection() 32 | .AddGitHubClientServices() 33 | .BuildServiceProvider(); 34 | 35 | // The client relies on the value from ${{ secrets.GITHUB_TOKEN }} 36 | var client = provider.GetRequiredService(); 37 | 38 | // Call GitHub REST API /repos/octokit/rest.js/pulls/123 39 | var pullRequest = client.Repos["octokit"]["rest.js"].Pulls[123].GetAsync(); 40 | 41 | Console.WriteLine(pullRequest.Title); 42 | ``` 43 | -------------------------------------------------------------------------------- /src/Actions.Octokit/Serialization/SourceGenerationContexts.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using CommonIssue = Actions.Octokit.Common.Issue; 5 | using CommonRepo = Actions.Octokit.Common.Repository; 6 | using Install = Actions.Octokit.Interfaces.Installation; 7 | using PR = Actions.Octokit.Interfaces.PullRequest; 8 | 9 | namespace Actions.Octokit.Serialization; 10 | 11 | [JsonSourceGenerationOptions( 12 | defaults: JsonSerializerDefaults.Web, 13 | WriteIndented = true, 14 | UseStringEnumConverter = true, 15 | AllowTrailingCommas = true, 16 | NumberHandling = JsonNumberHandling.AllowReadingFromString, 17 | PropertyNameCaseInsensitive = false, 18 | PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, 19 | IncludeFields = true)] 20 | [JsonSerializable(typeof(CommonIssue))] 21 | [JsonSerializable(typeof(CommonRepo))] 22 | [JsonSerializable(typeof(Context))] 23 | [JsonSerializable(typeof(Comment))] 24 | [JsonSerializable(typeof(Install))] 25 | [JsonSerializable(typeof(Owner))] 26 | [JsonSerializable(typeof(PayloadRepository))] 27 | [JsonSerializable(typeof(PR))] 28 | [JsonSerializable(typeof(Sender))] 29 | [JsonSerializable(typeof(WebhookIssue))] 30 | [JsonSerializable(typeof(WebhookPayload))] 31 | internal partial class SourceGenerationContexts : JsonSerializerContext 32 | { 33 | } 34 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GitHub.$(AssemblyName) 6 | 7 | 8 | 9 | GitHub Actions: Core .NET SDK ($(BasedOn)) 10 | This is an unofficial .NET SDK for GitHub Actions workflows (based on $(BasedOn)). 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/build/Common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 13 | root 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Actions.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(DefaultTargetFrameworks) 4 | $(NoWarn);IDE0005 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Commands/DefaultCommandIssuerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Commands; 5 | 6 | public sealed class DefaultCommandIssuerTests 7 | { 8 | [Fact] 9 | public void IssuesCorrectly() 10 | { 11 | var testConsole = new TestConsole(); 12 | var sut = new DefaultCommandIssuer(testConsole); 13 | 14 | sut.Issue( 15 | commandName: "command", 16 | message: "message"); 17 | 18 | Assert.Equal( 19 | expected: $""" 20 | Issuing unconventional command.{Environment.NewLine}::command::message{Environment.NewLine} 21 | """, 22 | actual: testConsole.Output.ToString()); 23 | } 24 | 25 | [Fact] 26 | public void IssuesCommandCorrectly() 27 | { 28 | var testConsole = new TestConsole(); 29 | var sut = new DefaultCommandIssuer(testConsole); 30 | 31 | sut.IssueCommand( 32 | commandName: "command", 33 | properties: null, 34 | message: "message"); 35 | 36 | Assert.Equal( 37 | expected: $""" 38 | Issuing unconventional command.{Environment.NewLine}::command::message{Environment.NewLine} 39 | """, 40 | actual: testConsole.Output.ToString()); 41 | } 42 | 43 | #pragma warning disable CA2211 // Non-constant fields should not be visible 44 | public static TheoryData?, string, string?> WritesOutputInput = 45 | #pragma warning restore CA2211 // Non-constant fields should not be visible 46 | new() 47 | { 48 | { 49 | new Dictionary 50 | { 51 | ["name"] = "summary" 52 | }, 53 | "Everything worked as expected", 54 | $"::{CommandNames.SetOutput} name=summary::Everything worked as expected" 55 | }, 56 | { 57 | null!, 58 | "deftones", 59 | $"::{CommandNames.SetOutput}::deftones" 60 | }, 61 | { 62 | new Dictionary 63 | { 64 | ["name"] = "percent % percent % cr \r cr \r lf \n lf \n colon : colon : comma , comma ," 65 | }, 66 | null!, 67 | $"::{CommandNames.SetOutput} name=percent %25 percent %25 cr %0D cr %0D lf %0A lf %0A colon %3A colon %3A comma %2C comma %2C::" 68 | }, 69 | { 70 | null!, 71 | "%25 %25 %0D %0D %0A %0A %3A %3A %2C %2C", 72 | $"::{CommandNames.SetOutput}::%2525 %2525 %250D %250D %250A %250A %253A %253A %252C %252C" 73 | }, 74 | { 75 | new Dictionary 76 | { 77 | ["prop1"] = "Value 1", 78 | ["prop2"] = "Value 2" 79 | }, 80 | "example", 81 | $"::{CommandNames.SetOutput} prop1=Value 1,prop2=Value 2::example" 82 | }, 83 | { 84 | new Dictionary 85 | { 86 | ["prop1"] = JsonSerializer.Serialize(new TestObject("object"), TestObjectContext.Default.TestObject), 87 | ["prop2"] = "123", 88 | ["prop3"] = "true" 89 | }, 90 | JsonSerializer.Serialize(new TestObject("object"), TestObjectContext.Default.TestObject).ToCommandValue(), 91 | $$""" 92 | ::{{CommandNames.SetOutput}} prop1={"test"%3A"object"},prop2=123,prop3=true::{"test":"object"} 93 | """ 94 | } 95 | }; 96 | 97 | [Theory] 98 | [MemberData(nameof(WritesOutputInput))] 99 | public void IssuesCommandWithPropertiesCorrectly( 100 | Dictionary? properties = null, 101 | string? message = null, 102 | string? expected = null) 103 | { 104 | var testConsole = new TestConsole(); 105 | var sut = new DefaultCommandIssuer(testConsole); 106 | 107 | sut.IssueCommand( 108 | commandName: CommandNames.SetOutput, 109 | properties, 110 | message); 111 | 112 | Assert.Equal( 113 | expected: $""" 114 | {expected}{Environment.NewLine} 115 | """, 116 | actual: testConsole.Output.ToString()); 117 | } 118 | } 119 | 120 | public record class TestObject([property: JsonPropertyName("test")] string Test); 121 | 122 | [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)] 123 | [JsonSerializable(typeof(TestObject))] 124 | internal sealed partial class TestObjectContext : JsonSerializerContext 125 | { 126 | } 127 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Commands/DefaultFileCommandIssuerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Commands; 5 | 6 | public sealed class DefaultFileCommandIssuerTests 7 | { 8 | [Fact] 9 | public async Task IssueFileCommandAsyncCorrectlyWritesFileTest() 10 | { 11 | var path = "test-file.txt"; 12 | Environment.SetEnvironmentVariable( 13 | $"GITHUB_TEST", 14 | path); 15 | await File.AppendAllTextAsync(path, ""); 16 | 17 | IFileCommandIssuer sut = new DefaultFileCommandIssuer( 18 | (filePath, actual) => 19 | { 20 | Assert.Fail("Anonymous types are not permitted."); 21 | 22 | return ValueTask.CompletedTask; 23 | }); 24 | 25 | await Assert.ThrowsAsync( 26 | async () => await sut.IssueFileCommandAsync( 27 | commandSuffix: "TEST", 28 | message: new { Test = "Values", Number = 7 })); 29 | } 30 | 31 | [Fact] 32 | public async Task SetOutputCorrectlySetsOutputTest() 33 | { 34 | var path = "set-output-test.txt"; 35 | try 36 | { 37 | Environment.SetEnvironmentVariable( 38 | "GITHUB_OUTPUT", 39 | path); 40 | await File.AppendAllTextAsync(path, ""); 41 | 42 | var services = new ServiceCollection(); 43 | services.AddGitHubActionsCore(); 44 | 45 | var provider = services.BuildServiceProvider(); 46 | var core = provider.GetRequiredService(); 47 | 48 | await core.SetOutputAsync("has-remaining-work", true); 49 | await core.SetOutputAsync("upgrade-projects", 50 | [ 51 | "this/is/a/test.csproj", 52 | "another/test/example.csproj" 53 | ], 54 | TestContext.Default.StringArray); 55 | 56 | var lines = await File.ReadAllLinesAsync(path); 57 | Assert.NotNull(lines); 58 | Assert.StartsWith("has-remaining-work", lines[0]); 59 | Assert.Equal("true", lines[1]); 60 | Assert.StartsWith("upgrade-projects", lines[3]); 61 | Assert.Equal("""["this/is/a/test.csproj","another/test/example.csproj"]""", lines[4]); 62 | } 63 | finally 64 | { 65 | File.Delete(path); 66 | 67 | Environment.SetEnvironmentVariable( 68 | "GITHUB_OUTPUT", 69 | null); 70 | } 71 | } 72 | } 73 | 74 | [JsonSerializable(typeof(string[]))] 75 | internal sealed partial class TestContext : JsonSerializerContext 76 | { 77 | } 78 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Extensions/ArgumentNullExceptionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Extensions; 5 | 6 | public sealed class ArgumentNullExceptionExtensionsTests 7 | { 8 | [Fact] 9 | public void ThrowIfNullExtensionCorrectlyCapturesParamNameTest() 10 | { 11 | object? pickles = default!; 12 | 13 | Assert.Throws( 14 | nameof(pickles), () => pickles!.ThrowIfNull()); 15 | } 16 | 17 | [Fact] 18 | public void ThrowIfNullExtensionCorrectlyYieldsValueTest() 19 | { 20 | var pickles = new { Test = true }; 21 | 22 | var result = pickles.ThrowIfNull(); 23 | 24 | Assert.True(result.Test); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Extensions/GenericExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Extensions; 5 | 6 | public sealed class GenericExtensionsTests 7 | { 8 | [Fact] 9 | public void ToCommandValueCorrectlyReturnsEmptyStringWhenNullTest() 10 | { 11 | string? value = null; 12 | var actual = value.ToCommandValue(); 13 | Assert.Equal(string.Empty, actual); 14 | } 15 | 16 | [Fact] 17 | public void ToCommandValueCorrectlyReturnsStringValueTest() 18 | { 19 | var value = "Hello!"; 20 | var actual = value.ToCommandValue(); 21 | Assert.Equal("Hello!", actual); 22 | } 23 | 24 | [Fact] 25 | public void ToCommandValueCorrectlySerializesValueTest() 26 | { 27 | SimpleObject actual = new( 28 | "David", 7, DateTime.Now, Guid.NewGuid(), [(decimal)Math.PI]); 29 | 30 | var typeInfo = SourceGenerationContexts.Default.SimpleObject; 31 | 32 | var commandValue = actual.ToCommandValue(typeInfo); 33 | 34 | var expected = JsonSerializer.Deserialize(commandValue, typeInfo); 35 | 36 | Assert.Equivalent(expected, actual); 37 | } 38 | } 39 | 40 | 41 | [JsonSerializable(typeof(SimpleObject))] 42 | internal sealed partial class SourceGenerationContexts : JsonSerializerContext 43 | { 44 | } 45 | 46 | internal sealed record class SimpleObject( 47 | string Name, 48 | int Number, 49 | DateTime Date, 50 | Guid Id, 51 | decimal[] Coordinates); 52 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Extensions/ObjectExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Extensions; 5 | 6 | public sealed class ObjectExtensionsTests 7 | { 8 | [Fact] 9 | public void ToCommandPropertiesCorrectlyCreatesSelfNamedKeyValuePairTest() 10 | { 11 | var key = "Value of 1, 2, 3..."; 12 | var actual = key.ToCommandProperties(); 13 | Assert.Equal("Value of 1, 2, 3...", actual[nameof(key)]); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Extensions; 5 | 6 | public sealed class ServiceCollectionExtensionsTests 7 | { 8 | [Fact] 9 | public void AddGitHubActionsCorrectlyRegistersServicesTest() 10 | { 11 | // Arrange / Act 12 | IServiceCollection services = new ServiceCollection(); 13 | _ = services.AddGitHubActionsCore(); 14 | 15 | var serviceTypes = 16 | services.Select(s => s.ServiceType); 17 | 18 | // Assert 19 | static bool AllTypesRegistered( 20 | IServiceCollection services, 21 | params (Type Type, ServiceLifetime Lifetime)[] types) 22 | { 23 | var serviceTypes = 24 | services.Select(s => (s.ServiceType, s.Lifetime)); 25 | 26 | return types.All(type => 27 | { 28 | Assert.Contains(type, serviceTypes); 29 | return true; 30 | }); 31 | } 32 | 33 | Assert.Equal(4, services.Count); 34 | Assert.DoesNotContain( 35 | services, 36 | descriptor => descriptor.ServiceType == typeof(TestConsole)); 37 | Assert.True(AllTypesRegistered( 38 | services, 39 | (typeof(IConsole), ServiceLifetime.Singleton), 40 | (typeof(ICommandIssuer), ServiceLifetime.Transient), 41 | (typeof(IFileCommandIssuer), ServiceLifetime.Transient), 42 | (typeof(ICoreService), ServiceLifetime.Transient))); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using System.Text; 5 | global using System.Text.Json; 6 | global using System.Text.Json.Serialization; 7 | 8 | global using Actions.Core.Commands; 9 | global using Actions.Core.Extensions; 10 | global using Actions.Core.Markdown; 11 | global using Actions.Core.Output; 12 | global using Actions.Core.Services; 13 | global using Actions.Core.Summaries; 14 | global using Actions.Core.Tests.Output; 15 | global using Actions.Core.Workflows; 16 | 17 | global using Actions.Core.EnvironmentVariables; 18 | 19 | global using Microsoft.Extensions.DependencyInjection; 20 | 21 | global using static Actions.Core.EnvironmentVariables.Keys; 22 | 23 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( 24 | "Performance", 25 | "CA1859:Use concrete types when possible for improved performance", 26 | Justification = "") 27 | ] 28 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Output/TestConsole.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Output; 5 | 6 | internal sealed class TestConsole : IConsole 7 | { 8 | public StringBuilder Output { get; } = new(); 9 | public StringBuilder ErrorOutput { get; } = new(); 10 | 11 | public int ExitCode { get; internal set; } 12 | public bool Exited { get; internal set; } 13 | 14 | public void ExitWithCode(int exitCode = 0) 15 | { 16 | (ExitCode, Exited) = (exitCode, true); 17 | } 18 | 19 | public void Write(string? message = null) 20 | { 21 | Output.Append(message); 22 | } 23 | 24 | public void WriteLine(string? message = null) 25 | { 26 | Output.AppendLine(message); 27 | } 28 | 29 | public void WriteError(string message) 30 | { 31 | ErrorOutput.Append(message); 32 | } 33 | 34 | public void WriteErrorLine(string message) 35 | { 36 | ErrorOutput.AppendLine(message); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Services/CoreSummaryTestFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Services; 5 | 6 | public sealed class CoreSummaryTestFixture 7 | { 8 | private readonly string TestDirectoryPath = Path.Combine( 9 | Directory.GetCurrentDirectory(), "test"); 10 | 11 | internal readonly string TestFilePath = Path.Combine( 12 | Directory.GetCurrentDirectory(), "test", "test-summary.md"); 13 | 14 | internal readonly TestCases TestCase = new(); 15 | 16 | internal sealed record class TestCases 17 | { 18 | internal readonly string Text = "hello world 🌎"; 19 | 20 | internal readonly string Code = """ 21 | func fork() { 22 | for { 23 | go fork() 24 | } 25 | } 26 | """; 27 | 28 | internal IEnumerable List = ["foo", "bar", "baz", "💣"]; 29 | 30 | internal IEnumerable Tasks = 31 | [ 32 | new TaskItem("foo"), 33 | new TaskItem("bar", true), 34 | new TaskItem("(Optional) baz"), 35 | new TaskItem("💣", false), 36 | ]; 37 | 38 | internal SummaryTable SummaryTable = new( 39 | Heading: new SummaryTableRow( 40 | [ 41 | new("foo", Alignment: TableColumnAlignment.Right), 42 | new("bar"), 43 | new("baz", Alignment: TableColumnAlignment.Left), 44 | ]), 45 | Rows: [ 46 | new SummaryTableRow( 47 | [ 48 | new("one"), 49 | new("two"), 50 | new("333"), 51 | ]), 52 | new SummaryTableRow( 53 | [ 54 | new("a"), 55 | new("b"), 56 | new("c"), 57 | ]) 58 | ]); 59 | 60 | internal SummaryTableRow[] Table = 61 | [ 62 | new SummaryTableRow( 63 | [ 64 | new("foo", true), 65 | new("bar", true), 66 | new("baz", true), 67 | new("tall", false, Rowspan: 3), 68 | ]), 69 | new SummaryTableRow( 70 | [ 71 | new("one"), 72 | new("two"), 73 | new("three"), 74 | ]), 75 | new SummaryTableRow( 76 | [ 77 | new("wide", Colspan: 3), 78 | ]) 79 | ]; 80 | 81 | internal (string Label, string Content) Details = ("open me", "🎉 surprise"); 82 | 83 | internal (string Src, string Alt, SummaryImageOptions Options) Img = 84 | ( 85 | "https://github.com/actions.png", 86 | "actions logo", 87 | new SummaryImageOptions(32, 32) 88 | ); 89 | 90 | internal (string Text, string Cite) Quote = ("Where the world builds software", "https://github.com/about"); 91 | 92 | internal (string Text, string Href) Link = ("GitHub", "https://github.com"); 93 | } 94 | 95 | internal void Test(Action testBody) 96 | { 97 | try 98 | { 99 | BeforeEach(); 100 | 101 | testBody.Invoke(); 102 | } 103 | finally 104 | { 105 | AfterEach(); 106 | } 107 | } 108 | 109 | internal async Task TestAsync(Func testBody) 110 | { 111 | try 112 | { 113 | BeforeEach(); 114 | 115 | await testBody.Invoke(); 116 | } 117 | finally 118 | { 119 | AfterEach(); 120 | } 121 | } 122 | 123 | private void BeforeEach() 124 | { 125 | Environment.SetEnvironmentVariable(GITHUB_STEP_SUMMARY, TestFilePath); 126 | Directory.CreateDirectory(TestDirectoryPath); 127 | File.WriteAllText(TestFilePath, ""); 128 | } 129 | 130 | private void AfterEach() 131 | { 132 | File.Delete(TestFilePath); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Services/DefaultWorkflowStepServiceTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Services; 5 | 6 | public sealed class DefaultWorkflowStepServiceTests 7 | { 8 | [Fact] 9 | public async Task DefaultWorkflowStepServiceAddsPathIssuesTest() 10 | { 11 | Environment.SetEnvironmentVariable(Keys.GITHUB_PATH, null); 12 | 13 | var testConsole = new TestConsole(); 14 | ICommandIssuer commandIssuer = new DefaultCommandIssuer(testConsole); 15 | IFileCommandIssuer fileCommandIssuer = new DefaultFileCommandIssuer( 16 | (filePath, actual) => ValueTask.CompletedTask); 17 | 18 | var sut = new DefaultCoreService( 19 | testConsole, commandIssuer, fileCommandIssuer); 20 | 21 | await sut.AddPathAsync("some/path/to/test"); 22 | 23 | Assert.Equal( 24 | expected: $""" 25 | ::add-path::some/path/to/test{Environment.NewLine} 26 | """, 27 | actual: testConsole.Output.ToString()); 28 | } 29 | 30 | [Fact] 31 | public async Task DefaultWorkflowStepServiceAddsPathCorrectlyTest() 32 | { 33 | var path = "test-001.txt"; 34 | Environment.SetEnvironmentVariable(Keys.GITHUB_PATH, path); 35 | await File.WriteAllTextAsync(path, ""); 36 | 37 | var testConsole = new TestConsole(); 38 | ICommandIssuer commandIssuer = new DefaultCommandIssuer(testConsole); 39 | IFileCommandIssuer fileCommandIssuer = new DefaultFileCommandIssuer( 40 | (filePath, actual) => 41 | { 42 | Assert.Equal("path/to/test", actual); 43 | 44 | return ValueTask.CompletedTask; 45 | }); 46 | 47 | var sut = new DefaultCoreService( 48 | testConsole, commandIssuer, fileCommandIssuer); 49 | 50 | await sut.AddPathAsync("path/to/test"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Actions.Core.Tests/Workflows/CommandTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Core.Tests.Workflows; 5 | 6 | public sealed class CommandTests 7 | { 8 | #pragma warning disable CA2211 // Non-constant fields should not be visible 9 | public static TheoryData?, string?> CommandToStringInput = 10 | #pragma warning restore CA2211 // Non-constant fields should not be visible 11 | new() 12 | { 13 | { 14 | "some-cmd", "7", null!, "::some-cmd::7" 15 | }, 16 | { 17 | "another-name", "true", null!, "::another-name::true" 18 | }, 19 | { 20 | "cmdr", "false", new Dictionary { ["k1"] = "v1" }, "::cmdr k1=v1::false" 21 | }, 22 | { 23 | "~~~", "Hi friends!", null!, "::~~~::Hi friends!" 24 | }, 25 | { 26 | null!, null!, null!, "::::" 27 | } 28 | }; 29 | 30 | [Theory] 31 | [MemberData(nameof(CommandToStringInput))] 32 | public void CommandToStringTest( 33 | string? name = null, 34 | string message = default!, 35 | Dictionary? properties = null, 36 | string? expected = null) 37 | { 38 | Command command = new(name, message, properties); 39 | 40 | var actual = command.ToString(); 41 | 42 | Assert.Equal(expected, actual); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/Actions.Glob.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(DefaultTargetFrameworks) 4 | $(NoWarn);IDE0005 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/Extensions/StringExtensionTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Actions.Glob.Tests; 6 | #pragma warning restore IDE0130 // Namespace does not match folder structure 7 | 8 | public sealed class StringExtensionsTests 9 | { 10 | public static readonly TheoryData GetGlobResultTestInput = 11 | new() 12 | { 13 | { 14 | "parent", 15 | new[] { "**/*" }, 16 | Array.Empty(), 17 | (true, 18 | [ 19 | "parent/file.md", 20 | "parent/README.md", 21 | "parent/child/file.MD", 22 | "parent/child/index.js", 23 | "parent/child/more.md", 24 | "parent/child/sample.mtext", 25 | "parent/child/assets/image.png", 26 | "parent/child/assets/image.svg", 27 | "parent/child/grandchild/file.md", 28 | "parent/child/grandchild/style.css", 29 | "parent/child/grandchild/sub.text" 30 | ]) 31 | }, 32 | { 33 | "parent", 34 | new[] { "**/*child/*.md" }, 35 | Array.Empty(), 36 | (true, 37 | [ 38 | "parent/child/file.MD", 39 | "parent/child/more.md", 40 | "parent/child/grandchild/file.md" 41 | ]) 42 | }, 43 | { 44 | "parent", 45 | new[] { "**/*/file.md" }, 46 | Array.Empty(), 47 | (true, 48 | [ 49 | "parent/child/file.MD", 50 | "parent/child/grandchild/file.md" 51 | ]) 52 | } 53 | }; 54 | 55 | [Theory, MemberData(nameof(GetGlobResultTestInput))] 56 | public void GetGlobResultTest( 57 | string directory, 58 | string[] includes, 59 | string[] excludes, 60 | (bool HasMatches, string[] Files) expected) 61 | { 62 | var actual = directory.GetGlobResult( 63 | includes, excludes); 64 | 65 | Assert.Equal(expected.HasMatches, actual.HasMatches); 66 | 67 | var actualFiles = actual.Files.Select(file => file.FullName).ToArray(); 68 | Assert.Equal(expected.Files?.Length, actualFiles?.Length); 69 | if (actual.HasMatches) 70 | { 71 | string[] expectedFiles = 72 | [ 73 | .. expected.Files!.Select( 74 | static file => Path.GetFullPath(file)) 75 | ]; 76 | 77 | Assert.All( 78 | expectedFiles, 79 | expectedFile => Assert.Contains(expectedFile, actualFiles!)); 80 | } 81 | } 82 | 83 | [Fact] 84 | public void GetGlobFilesTest() 85 | { 86 | var expectedFiles = new[] 87 | { 88 | "parent/file.md", 89 | "parent/README.md", 90 | "parent/child/file.MD", 91 | "parent/child/assets/image.svg", 92 | "parent/child/grandchild/file.md", 93 | }; 94 | 95 | var directory = "parent"; 96 | var actualFiles = directory.GetGlobFiles( 97 | ["**/*.md", "**/*.svg"], 98 | ["*/more.md"]) 99 | .ToArray(); 100 | 101 | string[] expected = 102 | [ 103 | .. expectedFiles.Select( 104 | static file => Path.GetFullPath(file)) 105 | ]; 106 | 107 | Assert.Equal(expectedFiles.Length, actualFiles.Length); 108 | Assert.All( 109 | expected, 110 | expectedFile => Assert.Contains(expectedFile, actualFiles!)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/GlobPatternBuilderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Glob.Tests; 5 | 6 | public sealed class GlobPatternBuilderTests 7 | { 8 | [Fact] 9 | public void GlobPatternBuilderThrowsWithoutIncludeOrExcludePatternsTest() 10 | { 11 | Assert.Throws( 12 | () => new DefaultGlobPatternResolverBuilder().Build()); 13 | } 14 | 15 | [Fact] 16 | public void GlobPatternBuilderYieldsWorkingResolver() 17 | { 18 | var resolver = new DefaultGlobPatternResolverBuilder() 19 | .WithInclusions("**/*.md", "**/*.svg") 20 | .WithExclusions("*/more.md") 21 | .Build(); 22 | 23 | var result = resolver.GetGlobResult("parent"); 24 | Assert.True(result.HasMatches); 25 | Assert.Equal(5, result.Files.Count()); 26 | 27 | string[] expectedFiles = 28 | [ 29 | "parent/file.md", 30 | "parent/README.md", 31 | "parent/child/file.MD", 32 | "parent/child/assets/image.svg", 33 | "parent/child/grandchild/file.md", 34 | ]; 35 | 36 | string[] expected = 37 | [ 38 | .. expectedFiles.Select( 39 | static file => Path.GetFullPath(file)) 40 | ]; 41 | 42 | Assert.All( 43 | expected, 44 | expectedFile => Assert.Contains( 45 | expectedFile, result.Files.Select(f => f.FullName))); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Xunit; -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/README.md -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/child/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/child/assets/image.png -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/child/assets/image.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/child/assets/image.svg -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/child/file.MD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/child/file.MD -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/child/grandchild/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/child/grandchild/file.md -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/child/grandchild/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/child/grandchild/style.css -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/child/grandchild/sub.text: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/child/grandchild/sub.text -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/child/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/child/index.js -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/child/more.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/child/more.md -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/child/sample.mtext: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/child/sample.mtext -------------------------------------------------------------------------------- /tests/Actions.Glob.Tests/parent/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/dotnet-github-actions-sdk/55816b043668d31a1db851652ea7d49f2ba7fadc/tests/Actions.Glob.Tests/parent/file.md -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/Actions.HttpClient.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(DefaultTargetFrameworks) 4 | $(NoWarn);IDE0005 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/Args.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Tests; 5 | 6 | public sealed record class Args 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/AuthTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Tests; 5 | 6 | [Trait("Category", "RequiresEnvVar")] 7 | public class AuthTests 8 | { 9 | public AuthTests() 10 | { 11 | Environment.SetEnvironmentVariable("no_proxy", null); 12 | Environment.SetEnvironmentVariable("http_proxy", null); 13 | Environment.SetEnvironmentVariable("https_proxy", null); 14 | } 15 | 16 | [Fact] 17 | public async Task HttpGetRequestWithBasicAuthCorrectlyDeserializesTypedResponse() 18 | { 19 | using var client = new ServiceCollection() 20 | .AddHttpClientServices() 21 | .BuildServiceProvider() 22 | .GetRequiredService() 23 | .CreateBasicClient("johndoe", "password"); 24 | 25 | var response = await client.GetAsync( 26 | "https://postman-echo.com/get", 27 | SourceGenerationContext.Default.PostmanEchoGetResponse); 28 | 29 | Assert.NotNull(response); 30 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 31 | Assert.NotNull(response.Result); 32 | var auth = response.Result.Headers["authorization"]; 33 | var creds = auth["Basic ".Length..].FromBase64(); 34 | Assert.Equal("johndoe:password", creds); 35 | } 36 | 37 | [Fact] 38 | public async Task HttpGetRequestWithBearerAuthCorrectlyDeserializesTypedResponse() 39 | { 40 | var token = "scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs"; 41 | 42 | using var client = new ServiceCollection() 43 | .AddHttpClientServices() 44 | .BuildServiceProvider() 45 | .GetRequiredService() 46 | .CreateBearerTokenClient(token); 47 | 48 | var response = await client.GetAsync( 49 | "https://postman-echo.com/get", 50 | SourceGenerationContext.Default.PostmanEchoGetResponse); 51 | 52 | Assert.NotNull(response); 53 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 54 | Assert.NotNull(response.Result); 55 | var auth = response.Result.Headers["authorization"]; 56 | Assert.Equal($"Bearer {token}", auth); 57 | } 58 | 59 | [Fact] 60 | public async Task HttpGetRequestWithPatAuthCorrectlyDeserializesTypedResponse() 61 | { 62 | var pat = "scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs"; 63 | 64 | using var client = new ServiceCollection() 65 | .AddHttpClientServices() 66 | .BuildServiceProvider() 67 | .GetRequiredService() 68 | .CreatePersonalAccessTokenClient(pat); 69 | 70 | var response = await client.GetAsync( 71 | "https://postman-echo.com/get", 72 | SourceGenerationContext.Default.PostmanEchoGetResponse); 73 | 74 | Assert.NotNull(response); 75 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 76 | Assert.NotNull(response.Result); 77 | var auth = response.Result.Headers["authorization"]; 78 | var creds = auth["Basic ".Length..].FromBase64(); 79 | Assert.Equal($"PAT:{pat}", creds); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using System.Net; 5 | global using System.Text.Json; 6 | global using System.Text.Json.Serialization; 7 | global using Actions.HttpClient.Extensions; 8 | 9 | global using Microsoft.Extensions.DependencyInjection; 10 | -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/HttpMethodExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Tests; 5 | 6 | public class HttpMethodExtensionsTests 7 | { 8 | [Theory] 9 | [InlineData("UNDEFINED", false)] 10 | [InlineData("HEAD", true)] 11 | [InlineData("OPTIONS", true)] 12 | [InlineData("DELETE", true)] 13 | [InlineData("GET", true)] 14 | [InlineData("PUT", false)] 15 | [InlineData("POST", false)] 16 | [InlineData("PATCH", false)] 17 | public void IsRetriableMethodCorrectlyEvaluatesEligibility(string method, bool expected) 18 | { 19 | var httpMethod = new HttpMethod(method); 20 | 21 | var actual = httpMethod.IsRetriableMethod(); 22 | 23 | Assert.Equal(expected, actual); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/PostmanEchoGetResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Tests; 5 | 6 | public record class PostmanEchoGetResponse( 7 | Args Args, 8 | Dictionary Headers, 9 | string Url); 10 | -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/PostmanEchoResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Tests; 5 | 6 | public sealed record class PostmanEchoResponse( 7 | RequestData Data, 8 | Args Args, 9 | Dictionary Headers, 10 | string Url); 11 | -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/RequestData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Tests; 5 | 6 | public sealed record RequestData(string Message); -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/ServiceCollectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Tests; 5 | 6 | public class ServiceCollectionExtensionsTests 7 | { 8 | [Fact] 9 | public void AddHttpClientServicesCorrectlyRegistersExpectedClients() 10 | { 11 | // Arrange 12 | var services = new ServiceCollection() 13 | .AddHttpClientServices(); 14 | 15 | // Act 16 | var provider = services.BuildServiceProvider(); 17 | var factory = provider.GetRequiredService(); 18 | 19 | // Assert 20 | Assert.NotNull(factory); 21 | Assert.IsType(factory); 22 | 23 | var basicClient = factory.CreateBasicClient("username", "password"); 24 | Assert.NotNull(basicClient); 25 | Assert.IsType(basicClient); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/SourceGenerationContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Tests; 5 | 6 | [JsonSourceGenerationOptions(defaults: JsonSerializerDefaults.Web)] 7 | [JsonSerializable(typeof(PostmanEchoGetResponse))] 8 | [JsonSerializable(typeof(PostmanEchoResponse))] 9 | [JsonSerializable(typeof(RequestData))] 10 | public partial class SourceGenerationContext : JsonSerializerContext 11 | { 12 | } -------------------------------------------------------------------------------- /tests/Actions.HttpClient.Tests/StringExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.HttpClient.Tests; 5 | 6 | public class StringExtensionsTests 7 | { 8 | [Fact] 9 | public void ToBase64ReturnsBase64EncodedString() 10 | { 11 | // Arrange 12 | var input = "Hello, World!"; 13 | 14 | // Act 15 | var result = StringExtensions.ToBase64(input); 16 | 17 | // Assert 18 | Assert.Equal("SGVsbG8sIFdvcmxkIQ==", result); 19 | } 20 | 21 | [Fact] 22 | public void FromBase64ReturnsDecodedString() 23 | { 24 | // Arrange 25 | var input = "SGVsbG8sIFdvcmxkIQ=="; 26 | 27 | // Act 28 | var result = StringExtensions.FromBase64(input); 29 | 30 | // Assert 31 | Assert.Equal("Hello, World!", result); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Actions.IO.Tests/Actions.IO.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(DefaultTargetFrameworks) 4 | $(NoWarn);IDE0005 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/Actions.IO.Tests/CopyOptionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.IO.Tests; 5 | 6 | public sealed class CopyOptionsTests 7 | { 8 | [Fact] 9 | public void CopyOptionsCorrectlyDefaultsProperties() 10 | { 11 | CopyOptions options = default; 12 | 13 | Assert.False(options.Recursive); 14 | Assert.False(options.Force); 15 | Assert.False(options.CopySourceDirectory); 16 | 17 | options = new(); 18 | 19 | Assert.False(options.Recursive); 20 | Assert.True(options.Force); 21 | Assert.True(options.CopySourceDirectory); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Actions.IO.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Xunit; -------------------------------------------------------------------------------- /tests/Actions.IO.Tests/MoveOptionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.IO.Tests; 5 | 6 | public sealed class MoveOptionsTests 7 | { 8 | [Fact] 9 | public void MoveOptionsCorrectlyDefaultsProperties() 10 | { 11 | MoveOptions options = default; 12 | 13 | Assert.False(options.Force); 14 | 15 | options = new(); 16 | 17 | Assert.True(options.Force); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Actions.IO.Tests/TempFolderTestFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.IO.Tests; 5 | 6 | public sealed class TempFolderTestFixture : IDisposable 7 | { 8 | internal string TempFolder { get; } = Directory.CreateDirectory( 9 | path: Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()) 10 | ) 11 | .FullName; 12 | 13 | void IDisposable.Dispose() 14 | { 15 | Directory.Delete(TempFolder, true); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/Actions.Octokit.Tests/Actions.Octokit.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(DefaultTargetFrameworks) 4 | $(NoWarn);IDE0005 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/Actions.Octokit.Tests/GitHubClientFactoryTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Tests; 5 | 6 | public class GitHubClientFactoryTests 7 | { 8 | [Fact] 9 | public void CreateThrowsOnNullToken() 10 | { 11 | Assert.Throws(() => GitHubClientFactory.Create(null!)); 12 | } 13 | 14 | [Fact] 15 | public void CreateThrowsOnEmptyToken() 16 | { 17 | Assert.Throws(() => GitHubClientFactory.Create(string.Empty)); 18 | } 19 | 20 | [Fact] 21 | public void CreateThrowsOnWhitespaceToken() 22 | { 23 | Assert.Throws(() => GitHubClientFactory.Create(" ")); 24 | } 25 | 26 | [Fact] 27 | public void CreateReturnsClientOnFakeToken() 28 | { 29 | Assert.NotNull(GitHubClientFactory.Create("token")); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Actions.Octokit.Tests/GitHubClientTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Actions.Octokit.Tests; 5 | 6 | public class GitHubClientTests 7 | { 8 | [Fact] 9 | public async Task GitHubClientGetsFirstPullRequestTest() 10 | { 11 | var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); 12 | 13 | if (token is null) 14 | { 15 | return; // Skip the test if the token isn't available. 16 | } 17 | 18 | var client = GitHubClientFactory.Create(token); 19 | 20 | var owner = "IEvangelist"; 21 | var repo = "dotnet-github-actions-sdk"; 22 | var pullNumber = 1; 23 | 24 | var firstPullRequest = await client.Repos[owner][repo].Pulls[pullNumber].GetAsync(); 25 | 26 | Assert.NotNull(firstPullRequest); 27 | } 28 | 29 | [Fact(Skip = "Not an actual test...")] 30 | public async Task RawHttpClientRestTest() 31 | { 32 | var client = new HttpClient(); 33 | 34 | var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); 35 | 36 | client.DefaultRequestHeaders.UserAgent.Add( 37 | new("Test", "0.1")); 38 | client.DefaultRequestHeaders.Authorization = 39 | new("Bearer", token); 40 | 41 | var owner = "DecimalTurn"; 42 | var repo = "VBA-on-GitHub-Automations"; 43 | var issueCommentId = 2310943199; 44 | 45 | try 46 | { 47 | var response = await client.GetAsync($""" 48 | https://api.github.com/repos/{owner}/{repo}/issues/comments/{issueCommentId} 49 | """); 50 | 51 | response.EnsureSuccessStatusCode(); 52 | 53 | var json = await response.Content.ReadAsStringAsync(); 54 | if (json is { }) 55 | { 56 | 57 | } 58 | } 59 | catch (Exception ex) 60 | { 61 | Assert.Fail(ex.Message); 62 | } 63 | } 64 | 65 | [Fact(Skip = "Upstream GitHub issue: https://github.com/octokit/dotnet-sdk/issues/117")] 66 | public async Task GitHubClientGetsIssueCommentTest() 67 | { 68 | var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); 69 | 70 | if (token is null) 71 | { 72 | return; // Skip the test if the token isn't available. 73 | } 74 | 75 | var client = GitHubClientFactory.Create(token); 76 | 77 | var owner = "DecimalTurn"; 78 | var repo = "VBA-on-GitHub-Automations"; 79 | var issueCommentId = 2310943199; 80 | 81 | try 82 | { 83 | var issueComment = 84 | await client.Repos[owner][repo].Issues.Comments[(int)issueCommentId].GetAsync(); 85 | 86 | Assert.NotNull(issueComment); 87 | } 88 | catch (Exception ex) 89 | { 90 | Assert.Fail(ex.Message); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Actions.Octokit.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) David Pine. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Xunit; 5 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------