Skip to content

Commit 9df969c

Browse files
Merge branch 'main' into copilot/fix-1348
2 parents ba8586a + a51bfe1 commit 9df969c

20 files changed

+371
-54
lines changed

.github/copilot-instructions.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
This is a C# based repository that produces several CLIs that are used by customers to interact with the GitHub migration APIs. Please follow these guidelines when contributing:
2+
3+
## Code Standards
4+
5+
### Required Before Each Commit
6+
- Run `dotnet format src/OctoshiftCLI.sln` before committing any changes to ensure proper code formatting. This will run dotnet format on all C# files to maintain consistent style
7+
8+
### Development Flow
9+
- Build: `dotnet build src/OctoshiftCLI.sln /p:TreatWarningsAsErrors=true`
10+
- Test: `dotnet test src/OctoshiftCLI.Tests/OctoshiftCLI.Tests.csproj`
11+
12+
## Repository Structure
13+
- `src/`: Contains the main C# source code for the Octoshift CLI
14+
- `src/ado2gh/`: Contains the ADO to GH CLI commands
15+
- `src/bbs2gh/`: Contains the BBS to GH CLI commands
16+
- `src/gei/`: Contains the GitHub to GitHub CLI commands
17+
- `src/Octoshift/`: Contains shared logic used by multiple commands/CLIs
18+
- `src/OctoshiftCLI.IntegrationTests/`: Contains integration tests for the Octoshift CLI
19+
- `src/OctoshiftCLI.Tests/`: Contains unit tests for the Octoshift CLI
20+
21+
## Key Guidelines
22+
1. Follow C# best practices and idiomatic patterns
23+
2. Maintain existing code structure and organization
24+
4. Write unit tests for new functionality.
25+
5. When making changes that would impact our users (e.g. new features or bug fixes), add a bullet point to `RELEASENOTES.md` with a user friendly brief description of the change
26+
6. Never silently swallow exceptions.
27+
7. If an exception is expected/understood and we can give a helpful user-friendly message, then throw an OctoshiftCliException with a user-friendly message. Otherwise let the exception bubble up and the top-level exception handler will log and handle it appropriately.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Setup Development Environment
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
paths:
7+
- .github/workflows/copilot-setup-steps.yml
8+
pull_request:
9+
paths:
10+
- .github/workflows/copilot-setup-steps.yml
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
# The job name MUST be 'copilot-setup-steps' to be picked up by GitHub Copilot
17+
copilot-setup-steps:
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Setup .NET
24+
uses: actions/setup-dotnet@v2
25+
with:
26+
global-json-file: global.json
27+
28+
- name: Restore dependencies
29+
run: dotnet restore src/OctoshiftCLI.sln

.github/workflows/integration-tests.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ on:
66
pr_number:
77
type: number
88
required: true
9+
sha:
10+
type: string
11+
required: true
12+
13+
permissions:
14+
contents: read
15+
checks: write
16+
pull-requests: write
917

1018
jobs:
1119
build-for-e2e-test:
@@ -20,6 +28,19 @@ jobs:
2028
ref: 'refs/pull/${{ github.event.inputs.pr_number }}/merge'
2129
fetch-depth: 0
2230

31+
- name: Check SHA
32+
run: |
33+
git fetch origin refs/pull/${{ github.event.inputs.pr_number }}/head:pr-head
34+
prsha=`git rev-parse pr-head | awk '{ print $1 }'`
35+
36+
echo "PR SHA: $prsha"
37+
echo "expected SHA: ${{ github.event.inputs.SHA }}"
38+
39+
if [ "$prsha" != "${{ github.event.inputs.SHA }}" ]; then
40+
echo "SHA must match" >&2
41+
exit 1
42+
fi
43+
2344
- name: Setup .NET
2445
uses: actions/setup-dotnet@v2
2546
with:
@@ -67,6 +88,7 @@ jobs:
6788
e2e-test:
6889
needs: [ build-for-e2e-test ]
6990
strategy:
91+
fail-fast: false
7092
matrix:
7193
runner-os: [windows-latest, ubuntu-latest, macos-latest]
7294
source-vcs: [AdoBasic, AdoCsv, Bbs, Ghes, Github]

RELEASENOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
- Fixed `ado2gh integrate-boards` command to properly report errors when GitHub PAT permissions are incorrect, instead of incorrectly reporting success.
12

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "8.0.404",
3+
"version": "8.0.410",
44
"rollForward": "minor"
55
}
66
}

src/Octoshift/Services/AdoApi.cs

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,16 +212,19 @@ public virtual async Task<string> GetGithubHandle(string org, string teamProject
212212
};
213213

214214
var response = await _client.PostAsync(url, payload);
215-
var data = JObject.Parse(response);
216215

217-
#pragma warning disable IDE0046 // Convert to conditional expression
218-
if (data["dataProviders"]["ms.vss-work-web.github-user-data-provider"] == null)
216+
// Check for error message in the response
217+
var errorMessage = ExtractErrorMessage(response, "ms.vss-work-web.github-user-data-provider");
218+
if (errorMessage.HasValue())
219219
{
220-
throw new OctoshiftCliException("Missing data from 'ms.vss-work-web.github-user-data-provider'. Please ensure the Azure DevOps project has a configured GitHub connection.");
220+
throw new OctoshiftCliException($"Error validating GitHub token: {errorMessage}");
221221
}
222-
#pragma warning restore IDE0046 // Convert to conditional expression
223222

224-
return (string)data["dataProviders"]["ms.vss-work-web.github-user-data-provider"]["login"];
223+
var data = JObject.Parse(response);
224+
var dataProviders = data["dataProviders"] ?? throw new OctoshiftCliException("Missing data from 'ms.vss-work-web.github-user-data-provider'. Please ensure the Azure DevOps project has a configured GitHub connection.");
225+
var dataProvider = dataProviders["ms.vss-work-web.github-user-data-provider"] ?? throw new OctoshiftCliException("Missing data from 'ms.vss-work-web.github-user-data-provider'. Please ensure the Azure DevOps project has a configured GitHub connection.");
226+
227+
return (string)dataProvider["login"];
225228
}
226229

227230
public virtual async Task<(string connectionId, string endpointId, string connectionName, IEnumerable<string> repoIds)> GetBoardsGithubConnection(string org, string teamProject)
@@ -329,7 +332,14 @@ public virtual async Task AddRepoToBoardsGithubConnection(string org, string tea
329332
}
330333
};
331334

332-
await _client.PostAsync(url, payload);
335+
var response = await _client.PostAsync(url, payload);
336+
337+
// Check for error message in the response
338+
var errorMessage = ExtractErrorMessage(response, "ms.vss-work-web.azure-boards-save-external-connection-data-provider");
339+
if (errorMessage.HasValue())
340+
{
341+
throw new OctoshiftCliException($"Error adding repository to boards GitHub connection: {errorMessage}");
342+
}
333343
}
334344

335345
public virtual async Task<string> GetTeamProjectId(string org, string teamProject)
@@ -600,9 +610,26 @@ public virtual async Task<string> GetBoardsGithubRepoId(string org, string teamP
600610
};
601611

602612
var response = await _client.PostAsync(url, payload);
613+
614+
// Check for error message in the response
615+
var errorMessage = ExtractErrorMessage(response, "ms.vss-work-web.github-user-repository-data-provider");
616+
if (errorMessage.HasValue())
617+
{
618+
throw new OctoshiftCliException($"Error getting GitHub repository information: {errorMessage}");
619+
}
620+
603621
var data = JObject.Parse(response);
622+
var dataProviders = data["dataProviders"] ?? throw new OctoshiftCliException("Could not retrieve GitHub repository information. Please verify the repository exists and the GitHub token has the correct permissions.");
623+
var dataProvider = dataProviders["ms.vss-work-web.github-user-repository-data-provider"];
624+
625+
#pragma warning disable IDE0046 // Convert to conditional expression
626+
if (dataProvider == null || dataProvider["additionalProperties"] == null || dataProvider["additionalProperties"]["nodeId"] == null)
627+
#pragma warning restore IDE0046 // Convert to conditional expression
628+
{
629+
throw new OctoshiftCliException("Could not retrieve GitHub repository information. Please verify the repository exists and the GitHub token has the correct permissions.");
630+
}
604631

605-
return (string)data["dataProviders"]["ms.vss-work-web.github-user-repository-data-provider"]["additionalProperties"]["nodeId"];
632+
return (string)dataProvider["additionalProperties"]["nodeId"];
606633
}
607634

608635
public virtual async Task CreateBoardsGithubConnection(string org, string teamProject, string endpointId, string repoId)
@@ -641,7 +668,14 @@ public virtual async Task CreateBoardsGithubConnection(string org, string teamPr
641668
}
642669
};
643670

644-
await _client.PostAsync(url, payload);
671+
var response = await _client.PostAsync(url, payload);
672+
673+
// Check for error message in the response
674+
var errorMessage = ExtractErrorMessage(response, "ms.vss-work-web.azure-boards-save-external-connection-data-provider");
675+
if (errorMessage.HasValue())
676+
{
677+
throw new OctoshiftCliException($"Error creating boards GitHub connection: {errorMessage}");
678+
}
645679
}
646680

647681
public virtual async Task DisableRepo(string org, string teamProject, string repoId)
@@ -700,6 +734,24 @@ public virtual async Task<bool> IsCallerOrgAdmin(string org)
700734
return await HasPermission(org, collectionSecurityNamespaceId, genericWritePermissionBitMaskValue);
701735
}
702736

737+
private string ExtractErrorMessage(string response, string dataProviderKey)
738+
{
739+
if (!response.HasValue())
740+
{
741+
return null;
742+
}
743+
744+
var data = JObject.Parse(response);
745+
#pragma warning disable IDE0046 // Convert to conditional expression
746+
if (data["dataProviders"] is not JObject dataProviders)
747+
{
748+
return null;
749+
}
750+
#pragma warning restore IDE0046 // Convert to conditional expression
751+
752+
return dataProviders[dataProviderKey] is not JObject dataProvider ? null : (string)dataProvider["errorMessage"];
753+
}
754+
703755
private async Task<bool> HasPermission(string org, string securityNamespaceId, int permission)
704756
{
705757
var response = await _client.GetAsync($"{_adoBaseUrl}/{org.EscapeDataString()}/_apis/permissions/{securityNamespaceId.EscapeDataString()}/{permission}?api-version=6.0");

0 commit comments

Comments
 (0)