├── .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 | 
4 |
5 | # GitHub Actions Workflow .NET SDK
6 |
7 | [](https://github.com/IEvangelist/dotnet-github-actions-sdk/actions/workflows/build-and-test.yml)
8 | [](https://github.com/IEvangelist/dotnet-github-actions-sdk/actions/workflows/codeql-analysis.yml)
9 | [](https://github.com/IEvangelist/dotnet-github-actions-sdk/actions/workflows/publish.yml)
10 | [](https://www.nuget.org/packages/GitHub.Actions.Core)
11 | [](#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 |
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 |
--------------------------------------------------------------------------------