From ecb4b5a9ea6d12de7eb6f87964cc5837cbb79296 Mon Sep 17 00:00:00 2001 From: Dariusz Porowski <3431813+DariuszPorowski@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:59:59 +0200 Subject: [PATCH 1/4] fix(core): update request method for ListItemConnections --- fabric/core/items_client.go | 2 +- internal/iruntime/service_client.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fabric/core/items_client.go b/fabric/core/items_client.go index bae5436..065c79d 100644 --- a/fabric/core/items_client.go +++ b/fabric/core/items_client.go @@ -444,7 +444,7 @@ func (client *ItemsClient) listItemConnectionsCreateRequest(ctx context.Context, return nil, errors.New("parameter itemID cannot be empty") } urlPath = strings.ReplaceAll(urlPath, "{itemId}", url.PathEscape(itemID)) - req, err := runtime.NewRequest(ctx, http.MethodPost, runtime.JoinPaths(client.endpoint, urlPath)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) if err != nil { return nil, err } diff --git a/internal/iruntime/service_client.go b/internal/iruntime/service_client.go index f54726f..ded01ff 100644 --- a/internal/iruntime/service_client.go +++ b/internal/iruntime/service_client.go @@ -19,7 +19,7 @@ import ( ) const ( - moduleVersion = "0.1.0-beta.3" + moduleVersion = "0.1.0-beta.4" defaultApiEndpoint = "https://api.fabric.microsoft.com" ) From 197737a21ed3e011df255710fa2370fb7a46ece8 Mon Sep 17 00:00:00 2001 From: Dariusz Porowski <3431813+DariuszPorowski@users.noreply.github.com> Date: Thu, 10 Oct 2024 23:14:52 +0200 Subject: [PATCH 2/4] ci(test): add workflow --- .gitattributes | 1 + .github/workflows/test.yml | 97 ++++++++++++++++++++++++++++++++++++++ .gitignore | 12 +++-- Taskfile.yml | 62 ++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 Taskfile.yml diff --git a/.gitattributes b/.gitattributes index c755c5c..597df47 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,7 @@ *.sh text eol=lf *.go text eol=lf *.ps1 text eol=lf +*.md text eol=lf # Declare files that will always have CRLF line endings on checkout. *.{cmd,[cC][mM][dD]} text eol=crlf diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0db1839 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,97 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +--- +name: ๐Ÿงช Test + +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + schedule: + - cron: "0 2 * * *" + workflow_dispatch: + +concurrency: + group: ${{ format('{0}-{1}-{2}-{3}-{4}', github.workflow, github.event_name, github.ref, github.base_ref || null, github.head_ref || null) }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + test: + name: ๐Ÿงช Test + runs-on: ubuntu-latest + steps: + - name: โคต๏ธ Checkout + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + + - name: ๐Ÿšง Setup Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: go.mod + cache: false + + - name: ๐Ÿšง Setup Task + uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0 + with: + repo-token: ${{ github.token }} + + - name: ๐Ÿ”จ Setup Test tools + run: task test:tools + + - name: ๐Ÿงช Run fake tests + run: task test + + - name: ๐Ÿ“ข Publish test results + if: always() + uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1 + with: + name: ๐Ÿ“œ Test results + reporter: jest-junit + path: testresults.xml + + - name: โš™๏ธ Get Coverage summary + uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 + with: + filename: coverage.xml + badge: true + fail_below_min: false + format: markdown + hide_branch_rate: false + hide_complexity: false + indicators: true + output: both + thresholds: "40 60" + + - name: ๐Ÿ“ค Upload test results + if: always() + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: test-results + path: testresults.xml + if-no-files-found: warn + overwrite: true + + - name: ๐Ÿ“ค Upload coverage results + if: always() + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: test-coverage-results + path: | + coverage.html + coverage.json + coverage.out + coverage.txt + coverage.xml + code-coverage-results.md + if-no-files-found: warn + overwrite: true + + - name: ๐Ÿ“ข Publish coverage results + run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 3b735ec..32c8330 100644 --- a/.gitignore +++ b/.gitignore @@ -11,11 +11,17 @@ # Test binary, built with `go test -c` *.test -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work + +# Test results +testresults.xml +coverage.out +coverage.json +coverage.txt +coverage.xml +coverage.html +code-coverage-results.md diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..5afa94e --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,62 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +# +# Windows: +# winget install Task.Task +# +# Linux: +# sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin +# echo 'command -v task >/dev/null || export PATH="$PATH:$HOME/.local/bin"' >> ~/.profile +# source ~/.profile +# +# macOS: +# brew install go-task/tap/go-task +--- +version: "3" + +vars: + PWSH: pwsh -NonInteractive -NoProfile -NoLogo -Command + +tasks: + test: + desc: Run tests + cmds: + - go clean -testcache + - '{{if eq .GITHUB_ACTIONS "true"}}gotestsum --format-hivis --format github-actions --junitfile "testresults.xml" -- ./... -p 4 -timeout 10m -coverprofile="coverage.out" -covermode atomic{{end}}' + - '{{if ne .GITHUB_ACTIONS "true"}}gotestsum --format-hivis --format pkgname-and-test-fails --junitfile "testresults.xml" -- ./... -p 4 -timeout 10m -coverprofile="coverage.out" -covermode atomic{{end}}' + - task: test:getcover + + test:getcover: + desc: Get test coverage results + internal: true + cmds: + - gocov convert coverage.out > coverage.json + - gocov report coverage.json > coverage.txt + - cmd: | + {{ .PWSH }} 'Get-Content coverage.json | gocov-xml > coverage.xml' + platforms: [windows] + - cmd: gocov-xml < coverage.json > coverage.xml + platforms: [linux, darwin] + - go tool cover -html coverage.out -o coverage.html + + test:tools: + desc: Install Test Tools + dir: "{{.goSdkOutput}}" + cmds: + - for: [gotestsum, gocov, gocov-xml] + task: install:{{.ITEM}} + + install:gotestsum: + desc: Install GoTestSum + cmds: + - go install gotest.tools/gotestsum@latest + + install:gocov: + desc: Install gocov + cmds: + - go install github.com/axw/gocov/gocov@latest + + install:gocov-xml: + desc: Install gocov-xml + cmds: + - go install github.com/AlekSi/gocov-xml@latest From 947efafd6d355b15c7ec5f3a0874c8e988f54e63 Mon Sep 17 00:00:00 2001 From: Dariusz Porowski <3431813+DariuszPorowski@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:19:01 +0200 Subject: [PATCH 3/4] feat: add public list artifact jobs --- fabric/core/fake/jobscheduler_server.go | 68 ++++++++++- fabric/core/fake_test.go | 105 ++++++++++++++++ fabric/core/jobscheduler_client.go | 112 ++++++++++++++++++ .../core/jobscheduler_client_example_test.go | 98 +++++++++++++++ fabric/core/models.go | 11 ++ fabric/core/models_serde.go | 35 ++++++ fabric/core/options.go | 7 ++ fabric/core/responses.go | 5 + .../backgroundjobs_client_example_test.go | 30 +++++ fabric/lakehouse/fake_test.go | 39 +++++- fabric/lakehouse/models.go | 6 +- fabric/lakehouse/models_serde.go | 4 + 12 files changed, 515 insertions(+), 5 deletions(-) diff --git a/fabric/core/fake/jobscheduler_server.go b/fabric/core/fake/jobscheduler_server.go index 27fd6df..9c3ab5c 100644 --- a/fabric/core/fake/jobscheduler_server.go +++ b/fabric/core/fake/jobscheduler_server.go @@ -20,6 +20,7 @@ import ( azfake "github.com/Azure/azure-sdk-for-go/sdk/azcore/fake" "github.com/Azure/azure-sdk-for-go/sdk/azcore/fake/server" "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/microsoft/fabric-sdk-go/fabric/core" ) @@ -42,6 +43,10 @@ type JobSchedulerServer struct { // HTTP status codes to indicate success: http.StatusOK GetItemSchedule func(ctx context.Context, workspaceID string, itemID string, jobType string, scheduleID string, options *core.JobSchedulerClientGetItemScheduleOptions) (resp azfake.Responder[core.JobSchedulerClientGetItemScheduleResponse], errResp azfake.ErrorResponder) + // NewListItemJobInstancesPager is the fake for method JobSchedulerClient.NewListItemJobInstancesPager + // HTTP status codes to indicate success: http.StatusOK + NewListItemJobInstancesPager func(workspaceID string, itemID string, options *core.JobSchedulerClientListItemJobInstancesOptions) (resp azfake.PagerResponder[core.JobSchedulerClientListItemJobInstancesResponse]) + // ListItemSchedules is the fake for method JobSchedulerClient.ListItemSchedules // HTTP status codes to indicate success: http.StatusOK ListItemSchedules func(ctx context.Context, workspaceID string, itemID string, jobType string, options *core.JobSchedulerClientListItemSchedulesOptions) (resp azfake.Responder[core.JobSchedulerClientListItemSchedulesResponse], errResp azfake.ErrorResponder) @@ -59,13 +64,17 @@ type JobSchedulerServer struct { // The returned JobSchedulerServerTransport instance is connected to an instance of core.JobSchedulerClient via the // azcore.ClientOptions.Transporter field in the client's constructor parameters. func NewJobSchedulerServerTransport(srv *JobSchedulerServer) *JobSchedulerServerTransport { - return &JobSchedulerServerTransport{srv: srv} + return &JobSchedulerServerTransport{ + srv: srv, + newListItemJobInstancesPager: newTracker[azfake.PagerResponder[core.JobSchedulerClientListItemJobInstancesResponse]](), + } } // JobSchedulerServerTransport connects instances of core.JobSchedulerClient to instances of JobSchedulerServer. // Don't use this type directly, use NewJobSchedulerServerTransport instead. type JobSchedulerServerTransport struct { - srv *JobSchedulerServer + srv *JobSchedulerServer + newListItemJobInstancesPager *tracker[azfake.PagerResponder[core.JobSchedulerClientListItemJobInstancesResponse]] } // Do implements the policy.Transporter interface for JobSchedulerServerTransport. @@ -96,6 +105,8 @@ func (j *JobSchedulerServerTransport) dispatchToMethodFake(req *http.Request, me res.resp, res.err = j.dispatchGetItemJobInstance(req) case "JobSchedulerClient.GetItemSchedule": res.resp, res.err = j.dispatchGetItemSchedule(req) + case "JobSchedulerClient.NewListItemJobInstancesPager": + res.resp, res.err = j.dispatchNewListItemJobInstancesPager(req) case "JobSchedulerClient.ListItemSchedules": res.resp, res.err = j.dispatchListItemSchedules(req) case "JobSchedulerClient.RunOnDemandItemJob": @@ -285,6 +296,59 @@ func (j *JobSchedulerServerTransport) dispatchGetItemSchedule(req *http.Request) return resp, nil } +func (j *JobSchedulerServerTransport) dispatchNewListItemJobInstancesPager(req *http.Request) (*http.Response, error) { + if j.srv.NewListItemJobInstancesPager == nil { + return nil, &nonRetriableError{errors.New("fake for method NewListItemJobInstancesPager not implemented")} + } + newListItemJobInstancesPager := j.newListItemJobInstancesPager.get(req) + if newListItemJobInstancesPager == nil { + const regexStr = `/v1/workspaces/(?P[!#&$-;=?-\[\]_a-zA-Z0-9~%@]+)/items/(?P[!#&$-;=?-\[\]_a-zA-Z0-9~%@]+)/jobs/instances` + regex := regexp.MustCompile(regexStr) + matches := regex.FindStringSubmatch(req.URL.EscapedPath()) + if matches == nil || len(matches) < 2 { + return nil, fmt.Errorf("failed to parse path %s", req.URL.Path) + } + qp := req.URL.Query() + workspaceIDParam, err := url.PathUnescape(matches[regex.SubexpIndex("workspaceId")]) + if err != nil { + return nil, err + } + itemIDParam, err := url.PathUnescape(matches[regex.SubexpIndex("itemId")]) + if err != nil { + return nil, err + } + continuationTokenUnescaped, err := url.QueryUnescape(qp.Get("continuationToken")) + if err != nil { + return nil, err + } + continuationTokenParam := getOptional(continuationTokenUnescaped) + var options *core.JobSchedulerClientListItemJobInstancesOptions + if continuationTokenParam != nil { + options = &core.JobSchedulerClientListItemJobInstancesOptions{ + ContinuationToken: continuationTokenParam, + } + } + resp := j.srv.NewListItemJobInstancesPager(workspaceIDParam, itemIDParam, options) + newListItemJobInstancesPager = &resp + j.newListItemJobInstancesPager.add(req, newListItemJobInstancesPager) + server.PagerResponderInjectNextLinks(newListItemJobInstancesPager, req, func(page *core.JobSchedulerClientListItemJobInstancesResponse, createLink func() string) { + page.ContinuationURI = to.Ptr(createLink()) + }) + } + resp, err := server.PagerResponderNext(newListItemJobInstancesPager, req) + if err != nil { + return nil, err + } + if !contains([]int{http.StatusOK}, resp.StatusCode) { + j.newListItemJobInstancesPager.remove(req) + return nil, &nonRetriableError{fmt.Errorf("unexpected status code %d. acceptable values are http.StatusOK", resp.StatusCode)} + } + if !server.PagerResponderMore(newListItemJobInstancesPager) { + j.newListItemJobInstancesPager.remove(req) + } + return resp, nil +} + func (j *JobSchedulerServerTransport) dispatchListItemSchedules(req *http.Request) (*http.Response, error) { if j.srv.ListItemSchedules == nil { return nil, &nonRetriableError{errors.New("fake for method ListItemSchedules not implemented")} diff --git a/fabric/core/fake_test.go b/fabric/core/fake_test.go index 358db06..062a3e8 100644 --- a/fabric/core/fake_test.go +++ b/fabric/core/fake_test.go @@ -1354,6 +1354,111 @@ func (testsuite *FakeTestSuite) TestJobScheduler_GetItemJobInstance() { testsuite.Require().True(reflect.DeepEqual(exampleRes, res.ItemJobInstance)) } +func (testsuite *FakeTestSuite) TestJobScheduler_ListItemJobInstances() { + // From example + ctx := runtime.WithHTTPHeader(testsuite.ctx, map[string][]string{ + "example-id": {"List item job instances example"}, + }) + var exampleWorkspaceID string + var exampleItemID string + exampleWorkspaceID = "4b218778-e7a5-4d73-8187-f10824047715" + exampleItemID = "431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7" + + exampleRes := core.ItemJobInstances{ + Value: []core.ItemJobInstance{ + { + EndTimeUTC: to.Ptr("2024-06-22T06:35:00.8033333"), + ID: to.Ptr("f2d65699-dd22-4889-980c-15226deb0e1b"), + InvokeType: to.Ptr(core.InvokeTypeManual), + ItemID: to.Ptr("431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7"), + JobType: to.Ptr("DefaultJob"), + RootActivityID: to.Ptr("8c2ee553-53a4-7edb-1042-0d8189a9e0ca"), + StartTimeUTC: to.Ptr("2024-06-22T06:35:00.7812154"), + Status: to.Ptr(core.StatusCompleted), + }, + { + EndTimeUTC: to.Ptr("2024-06-22T07:35:00.8033333"), + ID: to.Ptr("c0c99aed-be56-4fe0-a6e5-6de5fe277f16"), + InvokeType: to.Ptr(core.InvokeTypeManual), + ItemID: to.Ptr("431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7"), + JobType: to.Ptr("DefaultJob"), + RootActivityID: to.Ptr("c0c99aed-be56-4fe0-a6e5-6de5fe277f16"), + StartTimeUTC: to.Ptr("2024-06-22T06:35:00.7812154"), + Status: to.Ptr(core.StatusCompleted), + }}, + } + + testsuite.serverFactory.JobSchedulerServer.NewListItemJobInstancesPager = func(workspaceID string, itemID string, options *core.JobSchedulerClientListItemJobInstancesOptions) (resp azfake.PagerResponder[core.JobSchedulerClientListItemJobInstancesResponse]) { + testsuite.Require().Equal(exampleWorkspaceID, workspaceID) + testsuite.Require().Equal(exampleItemID, itemID) + resp = azfake.PagerResponder[core.JobSchedulerClientListItemJobInstancesResponse]{} + resp.AddPage(http.StatusOK, core.JobSchedulerClientListItemJobInstancesResponse{ItemJobInstances: exampleRes}, nil) + return + } + + client := testsuite.clientFactory.NewJobSchedulerClient() + pager := client.NewListItemJobInstancesPager(exampleWorkspaceID, exampleItemID, &core.JobSchedulerClientListItemJobInstancesOptions{ContinuationToken: nil}) + for pager.More() { + nextResult, err := pager.NextPage(ctx) + testsuite.Require().NoError(err, "Failed to advance page for example ") + testsuite.Require().True(reflect.DeepEqual(exampleRes, nextResult.ItemJobInstances)) + if err == nil { + break + } + } + + // From example + ctx = runtime.WithHTTPHeader(testsuite.ctx, map[string][]string{ + "example-id": {"List item job instances with continuation example"}, + }) + exampleWorkspaceID = "4b218778-e7a5-4d73-8187-f10824047715" + exampleItemID = "431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7" + + exampleRes = core.ItemJobInstances{ + ContinuationToken: to.Ptr("LDEsMTAwMDAwLDA%3D"), + ContinuationURI: to.Ptr("https://api.fabric.microsoft.com/v1/workspaces/4b218778-e7a5-4d73-8187-f10824047715/items/431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7/jobs/instances?continuationToken=LDEsMTAwMDAwLDA%3D"), + Value: []core.ItemJobInstance{ + { + EndTimeUTC: to.Ptr("2024-06-22T06:35:00.8033333"), + ID: to.Ptr("f2d65699-dd22-4889-980c-15226deb0e1b"), + InvokeType: to.Ptr(core.InvokeTypeManual), + ItemID: to.Ptr("431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7"), + JobType: to.Ptr("DefaultJob"), + RootActivityID: to.Ptr("8c2ee553-53a4-7edb-1042-0d8189a9e0ca"), + StartTimeUTC: to.Ptr("2024-06-22T06:35:00.7812154"), + Status: to.Ptr(core.StatusCompleted), + }, + { + EndTimeUTC: to.Ptr("2024-06-22T07:35:00.8033333"), + ID: to.Ptr("c0c99aed-be56-4fe0-a6e5-6de5fe277f16"), + InvokeType: to.Ptr(core.InvokeTypeManual), + ItemID: to.Ptr("431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7"), + JobType: to.Ptr("DefaultJob"), + RootActivityID: to.Ptr("c0c99aed-be56-4fe0-a6e5-6de5fe277f16"), + StartTimeUTC: to.Ptr("2024-06-22T06:35:00.7812154"), + Status: to.Ptr(core.StatusCompleted), + }}, + } + + testsuite.serverFactory.JobSchedulerServer.NewListItemJobInstancesPager = func(workspaceID string, itemID string, options *core.JobSchedulerClientListItemJobInstancesOptions) (resp azfake.PagerResponder[core.JobSchedulerClientListItemJobInstancesResponse]) { + testsuite.Require().Equal(exampleWorkspaceID, workspaceID) + testsuite.Require().Equal(exampleItemID, itemID) + resp = azfake.PagerResponder[core.JobSchedulerClientListItemJobInstancesResponse]{} + resp.AddPage(http.StatusOK, core.JobSchedulerClientListItemJobInstancesResponse{ItemJobInstances: exampleRes}, nil) + return + } + + pager = client.NewListItemJobInstancesPager(exampleWorkspaceID, exampleItemID, &core.JobSchedulerClientListItemJobInstancesOptions{ContinuationToken: nil}) + for pager.More() { + nextResult, err := pager.NextPage(ctx) + testsuite.Require().NoError(err, "Failed to advance page for example ") + testsuite.Require().True(reflect.DeepEqual(exampleRes, nextResult.ItemJobInstances)) + if err == nil { + break + } + } +} + func (testsuite *FakeTestSuite) TestJobScheduler_RunOnDemandItemJob() { // From example ctx := runtime.WithHTTPHeader(testsuite.ctx, map[string][]string{ diff --git a/fabric/core/jobscheduler_client.go b/fabric/core/jobscheduler_client.go index d84847d..eddb291 100644 --- a/fabric/core/jobscheduler_client.go +++ b/fabric/core/jobscheduler_client.go @@ -17,6 +17,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + + "github.com/microsoft/fabric-sdk-go/internal/iruntime" ) // JobSchedulerClient contains the methods for the JobScheduler group. @@ -340,6 +342,81 @@ func (client *JobSchedulerClient) getItemScheduleHandleResponse(resp *http.Respo return result, nil } +// NewListItemJobInstancesPager - REQUIRED DELEGATED SCOPES For item APIs use these scope types: +// * Generic scope: Item.ReadWrite.All or Item.Read.All +// +// * Specific scope: itemType.ReadWrite.All or itemType.Read.All (for example: Notebook.ReadWrite.All) +// +// for more information about scopes, see scopes article [/rest/api/fabric/articles/scopes]. +// +// MICROSOFT ENTRA SUPPORTED IDENTITIES This API supports the Microsoft identities [/rest/api/fabric/articles/identity-support] +// listed in this section. +// | Identity | Support | |-|-| | User | Yes | | Service principal [/entra/identity-platform/app-objects-and-service-principals#service-principal-object] +// | No | | Managed identities +// [/entra/identity/managed-identities-azure-resources/overview] | No | +// INTERFACE +// +// Generated from API version v1 +// - workspaceID - The workspace ID. +// - itemID - The item ID. +// - options - JobSchedulerClientListItemJobInstancesOptions contains the optional parameters for the JobSchedulerClient.NewListItemJobInstancesPager +// method. +func (client *JobSchedulerClient) NewListItemJobInstancesPager(workspaceID string, itemID string, options *JobSchedulerClientListItemJobInstancesOptions) *runtime.Pager[JobSchedulerClientListItemJobInstancesResponse] { + return runtime.NewPager(runtime.PagingHandler[JobSchedulerClientListItemJobInstancesResponse]{ + More: func(page JobSchedulerClientListItemJobInstancesResponse) bool { + return page.ContinuationURI != nil && len(*page.ContinuationURI) > 0 + }, + Fetcher: func(ctx context.Context, page *JobSchedulerClientListItemJobInstancesResponse) (JobSchedulerClientListItemJobInstancesResponse, error) { + ctx = context.WithValue(ctx, runtime.CtxAPINameKey{}, "core.JobSchedulerClient.NewListItemJobInstancesPager") + nextLink := "" + if page != nil { + nextLink = *page.ContinuationURI + } + resp, err := runtime.FetcherForNextLink(ctx, client.internal.Pipeline(), nextLink, func(ctx context.Context) (*policy.Request, error) { + return client.listItemJobInstancesCreateRequest(ctx, workspaceID, itemID, options) + }, nil) + if err != nil { + return JobSchedulerClientListItemJobInstancesResponse{}, err + } + return client.listItemJobInstancesHandleResponse(resp) + }, + Tracer: client.internal.Tracer(), + }) +} + +// listItemJobInstancesCreateRequest creates the ListItemJobInstances request. +func (client *JobSchedulerClient) listItemJobInstancesCreateRequest(ctx context.Context, workspaceID string, itemID string, options *JobSchedulerClientListItemJobInstancesOptions) (*policy.Request, error) { + urlPath := "/v1/workspaces/{workspaceId}/items/{itemId}/jobs/instances" + if workspaceID == "" { + return nil, errors.New("parameter workspaceID cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{workspaceId}", url.PathEscape(workspaceID)) + if itemID == "" { + return nil, errors.New("parameter itemID cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{itemId}", url.PathEscape(itemID)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + if options != nil && options.ContinuationToken != nil { + reqQP.Set("continuationToken", *options.ContinuationToken) + } + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// listItemJobInstancesHandleResponse handles the ListItemJobInstances response. +func (client *JobSchedulerClient) listItemJobInstancesHandleResponse(resp *http.Response) (JobSchedulerClientListItemJobInstancesResponse, error) { + result := JobSchedulerClientListItemJobInstancesResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.ItemJobInstances); err != nil { + return JobSchedulerClientListItemJobInstancesResponse{}, err + } + return result, nil +} + // ListItemSchedules - This API supports pagination [/rest/api/fabric/articles/pagination]. // REQUIRED DELEGATED SCOPES For item APIs use these scope types: // * Generic scope: Item.ReadWrite.All or Item.Read.All @@ -583,3 +660,38 @@ func (client *JobSchedulerClient) updateItemScheduleHandleResponse(resp *http.Re } // Custom code starts below + +// ListItemJobInstances - returns array of ItemJobInstance from all pages. +// REQUIRED DELEGATED SCOPES For item APIs use these scope types: +// +// - Generic scope: Item.ReadWrite.All or Item.Read.All +// +// - Specific scope: itemType.ReadWrite.All or itemType.Read.All (for example: Notebook.ReadWrite.All) +// +// for more information about scopes, see scopes article [/rest/api/fabric/articles/scopes]. +// +// MICROSOFT ENTRA SUPPORTED IDENTITIES This API supports the Microsoft identities [/rest/api/fabric/articles/identity-support] listed in this section. +// +// | Identity | Support | |-|-| | User | Yes | | Service principal [/entra/identity-platform/app-objects-and-service-principals#service-principal-object] | No | | Managed identities +// [/entra/identity/managed-identities-azure-resources/overview] | No | +// +// INTERFACE +// Generated from API version v1 +// - workspaceID - The workspace ID. +// - itemID - The item ID. +// - options - JobSchedulerClientListItemJobInstancesOptions contains the optional parameters for the JobSchedulerClient.NewListItemJobInstancesPager method. +func (client *JobSchedulerClient) ListItemJobInstances(ctx context.Context, workspaceID string, itemID string, options *JobSchedulerClientListItemJobInstancesOptions) ([]ItemJobInstance, error) { + pager := client.NewListItemJobInstancesPager(workspaceID, itemID, options) + mapper := func(resp JobSchedulerClientListItemJobInstancesResponse) []ItemJobInstance { + return resp.Value + } + list, err := iruntime.NewPageIterator(ctx, pager, mapper).Get() + if err != nil { + var azcoreRespError *azcore.ResponseError + if errors.As(err, &azcoreRespError) { + return []ItemJobInstance{}, NewResponseError(azcoreRespError.RawResponse) + } + return []ItemJobInstance{}, err + } + return list, nil +} diff --git a/fabric/core/jobscheduler_client_example_test.go b/fabric/core/jobscheduler_client_example_test.go index f609302..8d64d54 100644 --- a/fabric/core/jobscheduler_client_example_test.go +++ b/fabric/core/jobscheduler_client_example_test.go @@ -233,6 +233,104 @@ func ExampleJobSchedulerClient_GetItemJobInstance() { // } } +// Generated from example definition +func ExampleJobSchedulerClient_NewListItemJobInstancesPager_listItemJobInstancesExample() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + ctx := context.Background() + clientFactory, err := core.NewClientFactory(cred, nil, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + pager := clientFactory.NewJobSchedulerClient().NewListItemJobInstancesPager("4b218778-e7a5-4d73-8187-f10824047715", "431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7", &core.JobSchedulerClientListItemJobInstancesOptions{ContinuationToken: nil}) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + log.Fatalf("failed to advance page: %v", err) + } + for _, v := range page.Value { + // You could use page here. We use blank identifier for just demo purposes. + _ = v + } + // If the HTTP response code is 200 as defined in example definition, your page structure would look as follows. Please pay attention that all the values in the output are fake values for just demo purposes. + // page.ItemJobInstances = core.ItemJobInstances{ + // Value: []core.ItemJobInstance{ + // { + // EndTimeUTC: to.Ptr("2024-06-22T06:35:00.8033333"), + // ID: to.Ptr("f2d65699-dd22-4889-980c-15226deb0e1b"), + // InvokeType: to.Ptr(core.InvokeTypeManual), + // ItemID: to.Ptr("431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7"), + // JobType: to.Ptr("DefaultJob"), + // RootActivityID: to.Ptr("8c2ee553-53a4-7edb-1042-0d8189a9e0ca"), + // StartTimeUTC: to.Ptr("2024-06-22T06:35:00.7812154"), + // Status: to.Ptr(core.StatusCompleted), + // }, + // { + // EndTimeUTC: to.Ptr("2024-06-22T07:35:00.8033333"), + // ID: to.Ptr("c0c99aed-be56-4fe0-a6e5-6de5fe277f16"), + // InvokeType: to.Ptr(core.InvokeTypeManual), + // ItemID: to.Ptr("431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7"), + // JobType: to.Ptr("DefaultJob"), + // RootActivityID: to.Ptr("c0c99aed-be56-4fe0-a6e5-6de5fe277f16"), + // StartTimeUTC: to.Ptr("2024-06-22T06:35:00.7812154"), + // Status: to.Ptr(core.StatusCompleted), + // }}, + // } + } +} + +// Generated from example definition +func ExampleJobSchedulerClient_NewListItemJobInstancesPager_listItemJobInstancesWithContinuationExample() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + ctx := context.Background() + clientFactory, err := core.NewClientFactory(cred, nil, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + pager := clientFactory.NewJobSchedulerClient().NewListItemJobInstancesPager("4b218778-e7a5-4d73-8187-f10824047715", "431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7", &core.JobSchedulerClientListItemJobInstancesOptions{ContinuationToken: nil}) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + log.Fatalf("failed to advance page: %v", err) + } + for _, v := range page.Value { + // You could use page here. We use blank identifier for just demo purposes. + _ = v + } + // If the HTTP response code is 200 as defined in example definition, your page structure would look as follows. Please pay attention that all the values in the output are fake values for just demo purposes. + // page.ItemJobInstances = core.ItemJobInstances{ + // ContinuationToken: to.Ptr("LDEsMTAwMDAwLDA%3D"), + // ContinuationURI: to.Ptr("https://api.fabric.microsoft.com/v1/workspaces/4b218778-e7a5-4d73-8187-f10824047715/items/431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7/jobs/instances?continuationToken=LDEsMTAwMDAwLDA%3D"), + // Value: []core.ItemJobInstance{ + // { + // EndTimeUTC: to.Ptr("2024-06-22T06:35:00.8033333"), + // ID: to.Ptr("f2d65699-dd22-4889-980c-15226deb0e1b"), + // InvokeType: to.Ptr(core.InvokeTypeManual), + // ItemID: to.Ptr("431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7"), + // JobType: to.Ptr("DefaultJob"), + // RootActivityID: to.Ptr("8c2ee553-53a4-7edb-1042-0d8189a9e0ca"), + // StartTimeUTC: to.Ptr("2024-06-22T06:35:00.7812154"), + // Status: to.Ptr(core.StatusCompleted), + // }, + // { + // EndTimeUTC: to.Ptr("2024-06-22T07:35:00.8033333"), + // ID: to.Ptr("c0c99aed-be56-4fe0-a6e5-6de5fe277f16"), + // InvokeType: to.Ptr(core.InvokeTypeManual), + // ItemID: to.Ptr("431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7"), + // JobType: to.Ptr("DefaultJob"), + // RootActivityID: to.Ptr("c0c99aed-be56-4fe0-a6e5-6de5fe277f16"), + // StartTimeUTC: to.Ptr("2024-06-22T06:35:00.7812154"), + // Status: to.Ptr(core.StatusCompleted), + // }}, + // } + } +} + // Generated from example definition func ExampleJobSchedulerClient_RunOnDemandItemJob_runItemJobInstanceWithNoRequestBodyExample() { cred, err := azidentity.NewDefaultAzureCredential(nil) diff --git a/fabric/core/models.go b/fabric/core/models.go index 55740ba..0ee0a33 100644 --- a/fabric/core/models.go +++ b/fabric/core/models.go @@ -957,6 +957,17 @@ type ItemJobInstance struct { FailureReason *ErrorResponse } +type ItemJobInstances struct { + // REQUIRED; A list of item job instances. + Value []ItemJobInstance + + // The token for the next result set batch. If there are no more records, it's removed from the response. + ContinuationToken *string + + // The URI of the next result set batch. If there are no more records, it's removed from the response. + ContinuationURI *string +} + // ItemMetadata - Contains the item metadata. type ItemMetadata struct { // READ-ONLY; The display name of the item. Prefers the workspace item's display name if it exists, otherwise displayName diff --git a/fabric/core/models_serde.go b/fabric/core/models_serde.go index 50b16ec..8ca8de7 100644 --- a/fabric/core/models_serde.go +++ b/fabric/core/models_serde.go @@ -2289,6 +2289,41 @@ func (i *ItemJobInstance) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaller interface for type ItemJobInstances. +func (i ItemJobInstances) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "continuationToken", i.ContinuationToken) + populate(objectMap, "continuationUri", i.ContinuationURI) + populate(objectMap, "value", i.Value) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type ItemJobInstances. +func (i *ItemJobInstances) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", i, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "continuationToken": + err = unpopulate(val, "ContinuationToken", &i.ContinuationToken) + delete(rawMsg, key) + case "continuationUri": + err = unpopulate(val, "ContinuationURI", &i.ContinuationURI) + delete(rawMsg, key) + case "value": + err = unpopulate(val, "Value", &i.Value) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", i, err) + } + } + return nil +} + // MarshalJSON implements the json.Marshaller interface for type ItemMetadata. func (i ItemMetadata) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) diff --git a/fabric/core/options.go b/fabric/core/options.go index c700b95..89d6838 100644 --- a/fabric/core/options.go +++ b/fabric/core/options.go @@ -195,6 +195,13 @@ type JobSchedulerClientGetItemScheduleOptions struct { // placeholder for future optional parameters } +// JobSchedulerClientListItemJobInstancesOptions contains the optional parameters for the JobSchedulerClient.NewListItemJobInstancesPager +// method. +type JobSchedulerClientListItemJobInstancesOptions struct { + // A token for retrieving the next page of results. + ContinuationToken *string +} + // JobSchedulerClientListItemSchedulesOptions contains the optional parameters for the JobSchedulerClient.ListItemSchedules // method. type JobSchedulerClientListItemSchedulesOptions struct { diff --git a/fabric/core/responses.go b/fabric/core/responses.go index 6d24194..64a048b 100644 --- a/fabric/core/responses.go +++ b/fabric/core/responses.go @@ -181,6 +181,11 @@ type JobSchedulerClientGetItemScheduleResponse struct { ItemSchedule } +// JobSchedulerClientListItemJobInstancesResponse contains the response from method JobSchedulerClient.NewListItemJobInstancesPager. +type JobSchedulerClientListItemJobInstancesResponse struct { + ItemJobInstances +} + // JobSchedulerClientListItemSchedulesResponse contains the response from method JobSchedulerClient.ListItemSchedules. type JobSchedulerClientListItemSchedulesResponse struct { // list of schedules for this item. diff --git a/fabric/lakehouse/backgroundjobs_client_example_test.go b/fabric/lakehouse/backgroundjobs_client_example_test.go index 5e4e982..43098c1 100644 --- a/fabric/lakehouse/backgroundjobs_client_example_test.go +++ b/fabric/lakehouse/backgroundjobs_client_example_test.go @@ -16,6 +16,36 @@ import ( "github.com/microsoft/fabric-sdk-go/fabric/lakehouse" ) +// Generated from example definition +func ExampleBackgroundJobsClient_RunOnDemandTableMaintenance_runTableMaintenanceWithOptimizeZOrderAndVacuumEnabledForSchemaEnabledLakehouse() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + ctx := context.Background() + clientFactory, err := lakehouse.NewClientFactory(cred, nil, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + _, err = clientFactory.NewBackgroundJobsClient().RunOnDemandTableMaintenance(ctx, "4b218778-e7a5-4d73-8187-f10824047715", "431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7", "TableMaintenance", lakehouse.RunOnDemandTableMaintenanceRequest{ + ExecutionData: &lakehouse.TableMaintenanceExecutionData{ + OptimizeSettings: &lakehouse.OptimizeSettings{ + VOrder: to.Ptr(true), + ZOrderBy: []string{ + "tipAmount"}, + }, + SchemaName: to.Ptr("dbo"), + TableName: to.Ptr("table1"), + VacuumSettings: &lakehouse.VacuumSettings{ + RetentionPeriod: to.Ptr("7:01:00:00"), + }, + }, + }, nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } +} + // Generated from example definition func ExampleBackgroundJobsClient_RunOnDemandTableMaintenance_runTableMaintenanceWithOptimizeZOrderAndVacuumEnabled() { cred, err := azidentity.NewDefaultAzureCredential(nil) diff --git a/fabric/lakehouse/fake_test.go b/fabric/lakehouse/fake_test.go index 29a87dd..a429c71 100644 --- a/fabric/lakehouse/fake_test.go +++ b/fabric/lakehouse/fake_test.go @@ -355,7 +355,7 @@ func (testsuite *FakeTestSuite) TestTables_ListTables() { func (testsuite *FakeTestSuite) TestBackgroundJobs_RunOnDemandTableMaintenance() { // From example ctx := runtime.WithHTTPHeader(testsuite.ctx, map[string][]string{ - "example-id": {"Run table maintenance with optimize Z-Order and vacuum enabled."}, + "example-id": {"Run table maintenance with optimize Z-Order and vacuum enabled for schema enabled lakehouse."}, }) var exampleWorkspaceID string var exampleLakehouseID string @@ -371,7 +371,8 @@ func (testsuite *FakeTestSuite) TestBackgroundJobs_RunOnDemandTableMaintenance() ZOrderBy: []string{ "tipAmount"}, }, - TableName: to.Ptr("table1"), + SchemaName: to.Ptr("dbo"), + TableName: to.Ptr("table1"), VacuumSettings: &lakehouse.VacuumSettings{ RetentionPeriod: to.Ptr("7:01:00:00"), }, @@ -392,6 +393,40 @@ func (testsuite *FakeTestSuite) TestBackgroundJobs_RunOnDemandTableMaintenance() _, err = client.RunOnDemandTableMaintenance(ctx, exampleWorkspaceID, exampleLakehouseID, exampleJobType, exampleRunOnDemandTableMaintenanceRequest, nil) testsuite.Require().NoError(err, "Failed to get result for example ") + // From example + ctx = runtime.WithHTTPHeader(testsuite.ctx, map[string][]string{ + "example-id": {"Run table maintenance with optimize Z-Order and vacuum enabled."}, + }) + exampleWorkspaceID = "4b218778-e7a5-4d73-8187-f10824047715" + exampleLakehouseID = "431e8d7b-4a95-4c02-8ccd-6faef5ba1bd7" + exampleJobType = "TableMaintenance" + exampleRunOnDemandTableMaintenanceRequest = lakehouse.RunOnDemandTableMaintenanceRequest{ + ExecutionData: &lakehouse.TableMaintenanceExecutionData{ + OptimizeSettings: &lakehouse.OptimizeSettings{ + VOrder: to.Ptr(true), + ZOrderBy: []string{ + "tipAmount"}, + }, + TableName: to.Ptr("table1"), + VacuumSettings: &lakehouse.VacuumSettings{ + RetentionPeriod: to.Ptr("7:01:00:00"), + }, + }, + } + + testsuite.serverFactory.BackgroundJobsServer.RunOnDemandTableMaintenance = func(ctx context.Context, workspaceID string, lakehouseID string, jobType string, runOnDemandTableMaintenanceRequest lakehouse.RunOnDemandTableMaintenanceRequest, options *lakehouse.BackgroundJobsClientRunOnDemandTableMaintenanceOptions) (resp azfake.Responder[lakehouse.BackgroundJobsClientRunOnDemandTableMaintenanceResponse], errResp azfake.ErrorResponder) { + testsuite.Require().Equal(exampleWorkspaceID, workspaceID) + testsuite.Require().Equal(exampleLakehouseID, lakehouseID) + testsuite.Require().Equal(exampleJobType, jobType) + testsuite.Require().True(reflect.DeepEqual(exampleRunOnDemandTableMaintenanceRequest, runOnDemandTableMaintenanceRequest)) + resp = azfake.Responder[lakehouse.BackgroundJobsClientRunOnDemandTableMaintenanceResponse]{} + resp.SetResponse(http.StatusAccepted, lakehouse.BackgroundJobsClientRunOnDemandTableMaintenanceResponse{}, nil) + return + } + + _, err = client.RunOnDemandTableMaintenance(ctx, exampleWorkspaceID, exampleLakehouseID, exampleJobType, exampleRunOnDemandTableMaintenanceRequest, nil) + testsuite.Require().NoError(err, "Failed to get result for example ") + // From example ctx = runtime.WithHTTPHeader(testsuite.ctx, map[string][]string{ "example-id": {"Run table maintenance with optimize enabled and vacuum disabled."}, diff --git a/fabric/lakehouse/models.go b/fabric/lakehouse/models.go index ad0c860..fd45663 100644 --- a/fabric/lakehouse/models.go +++ b/fabric/lakehouse/models.go @@ -180,12 +180,16 @@ type Table struct { // TableMaintenanceExecutionData - Run on demand lakehouse table maintenance instance payload type TableMaintenanceExecutionData struct { - // REQUIRED; Name of the table to run maintenance on. + // REQUIRED; Name of the table to run maintenance on. Max length of 256 character alphanumeric string with underscores. TableName *string // Configures the optimization settings of the maintenance job. To skip table optimization, leave this parameter empty. OptimizeSettings *OptimizeSettings + // Name of the schema under which the table is created. This property is applicable only for a schema enabled Lakehouse. Max + // length of 128 character alphanumeric string with underscores. + SchemaName *string + // Configures the vacuum [https://docs.delta.io/latest/delta-utility.html#-delta-vacuum] settings of the maintenance job. // To skip table vacuum, leave this parameter empty. VacuumSettings *VacuumSettings diff --git a/fabric/lakehouse/models_serde.go b/fabric/lakehouse/models_serde.go index 2f23cab..b1f399f 100644 --- a/fabric/lakehouse/models_serde.go +++ b/fabric/lakehouse/models_serde.go @@ -469,6 +469,7 @@ func (t *Table) UnmarshalJSON(data []byte) error { func (t TableMaintenanceExecutionData) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) populate(objectMap, "optimizeSettings", t.OptimizeSettings) + populate(objectMap, "schemaName", t.SchemaName) populate(objectMap, "tableName", t.TableName) populate(objectMap, "vacuumSettings", t.VacuumSettings) return json.Marshal(objectMap) @@ -486,6 +487,9 @@ func (t *TableMaintenanceExecutionData) UnmarshalJSON(data []byte) error { case "optimizeSettings": err = unpopulate(val, "OptimizeSettings", &t.OptimizeSettings) delete(rawMsg, key) + case "schemaName": + err = unpopulate(val, "SchemaName", &t.SchemaName) + delete(rawMsg, key) case "tableName": err = unpopulate(val, "TableName", &t.TableName) delete(rawMsg, key) From dde525b3c890599aca6f9c7503ea56c02b530f08 Mon Sep 17 00:00:00 2001 From: Dariusz Porowski <3431813+DariuszPorowski@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:26:51 +0200 Subject: [PATCH 4/4] ci(test): add coverage results publishing step --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0db1839..39fa365 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,6 +69,9 @@ jobs: output: both thresholds: "40 60" + - name: ๐Ÿ“ข Publish coverage results + run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY + - name: ๐Ÿ“ค Upload test results if: always() uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 @@ -92,6 +95,3 @@ jobs: code-coverage-results.md if-no-files-found: warn overwrite: true - - - name: ๐Ÿ“ข Publish coverage results - run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY