Skip to content

Commit 9aed007

Browse files
committed
feat(script): Add logic to generate a Nuget.config and skip already uploaded packages.
1 parent 5b36243 commit 9aed007

5 files changed

Lines changed: 134 additions & 78 deletions

File tree

.github/workflows/sync-nuget.yml

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,32 +54,8 @@ jobs:
5454
python -m pip install --upgrade pip
5555
pip install -e .
5656
57-
- name: Create NuGet config
58-
run: |
59-
mkdir -p ~/.nuget/NuGet
60-
echo '<?xml version="1.0" encoding="utf-8"?>' > ~/.nuget/NuGet/NuGet.Config
61-
echo '<configuration>' >> ~/.nuget/NuGet/NuGet.Config
62-
echo ' <packageSources>' >> ~/.nuget/NuGet/NuGet.Config
63-
echo ' <add key="keyfactor" value="https://pkgs.dev.azure.com/Keyfactor/_packaging/KeyfactorPackages/nuget/v3/index.json" />' >> ~/.nuget/NuGet/NuGet.Config
64-
echo ' <add key="github" value="https://nuget.pkg.github.com/Keyfactor/index.json" />' >> ~/.nuget/NuGet/NuGet.Config
65-
echo ' </packageSources>' >> ~/.nuget/NuGet/NuGet.Config
66-
echo ' <packageSourceCredentials>' >> ~/.nuget/NuGet/NuGet.Config
67-
echo ' <github>' >> ~/.nuget/NuGet/NuGet.Config
68-
echo ' <add key="Username" value="spbsoluble" />' >> ~/.nuget/NuGet/NuGet.Config
69-
echo ' <add key="ClearTextPassword" value="${{ secrets.GH_NUGET_TOKEN }}" />' >> ~/.nuget/NuGet/NuGet.Config
70-
echo ' </github>' >> ~/.nuget/NuGet/NuGet.Config
71-
echo ' <keyfactor>' >> ~/.nuget/NuGet/NuGet.Config
72-
echo ' <add key="Username" value="AzureDevOps" />' >> ~/.nuget/NuGet/NuGet.Config
73-
echo ' <add key="ClearTextPassword" value="${{ secrets.AZ_DEVOPS_PAT }}" />' >> ~/.nuget/NuGet/NuGet.Config
74-
echo ' </keyfactor>' >> ~/.nuget/NuGet/NuGet.Config
75-
echo ' </packageSourceCredentials>' >> ~/.nuget/NuGet/NuGet.Config
76-
echo '</configuration>' >> ~/.nuget/NuGet/NuGet.Config
77-
cat ~/.nuget/NuGet/NuGet.Config
78-
env:
79-
GITHUB_TOKEN: ${{ secrets.GH_NUGET_TOKEN }}
80-
8157
- name: Run NuGet sync script
8258
run: python scripts/sync_nuget.py
8359
env:
8460
GITHUB_TOKEN: ${{ secrets.GH_NUGET_TOKEN }}
85-
# Add any other environment variables needed by your script
61+
AZ_DEVOPS_PAT: ${{ secrets.AZ_DEVOPS_PAT }}

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ _site
44
.jekyll-metadata
55
vendor
66

7-
.vs
7+
.vs
8+
*.env
9+
.claude
10+
CLAUDE.md
11+
nupkgs/*

README.md

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,49 @@
22

33
Publicly available NuGet packages useful for building Keyfactor integrations
44

5-
To access these packages add https://nuget.pkg.github.com/Keyfactor/index.json as a NuGet package source to Visual
6-
Studio or other development tools.
5+
To access these packages add `https://nuget.pkg.github.com/Keyfactor/index.json` as a NuGet package source in Visual Studio or other development tools.
76

8-
Some package availability changes from time to time. Please view the `CHANGELOG.md` for further details on changes to
9-
available libraries.
7+
Some package availability changes from time to time. Please view the `CHANGELOG.md` for further details on changes to available libraries.
108

11-
## Adding a Nuget Package and/or Version
9+
## Adding a NuGet Package and/or Version
1210

1311
### Via GitHub Actions
1412

15-
To add a Nuget package modify the [packages.yml](./packages.yml) file in the root of this repository.
13+
1. Add the package and version(s) to [`packages.yml`](./packages.yml).
14+
2. Trigger the [Sync NuGet Packages](https://github.com/Keyfactor/public-nuget-packages/actions/workflows/sync-nuget.yml) workflow.
15+
16+
Once complete, the package will be available in the Keyfactor Public GitHub Package Registry (GPR). The workflow runs automatically on a weekly schedule and skips any versions already published.
1617

1718
> [!IMPORTANT]
18-
> The package and version must exist in this Nuget repository before it can be added to the `packages.yml` file.
19+
> The package and version must already exist in the Azure DevOps feed before adding it to `packages.yml`:
1920
> https://dev.azure.com/Keyfactor/Engineering/_artifacts/feed/KeyfactorPackages@Local
2021
21-
The to trigger the sync run the
22-
workflow [Sync NuGet Packages](https://github.com/Keyfactor/public-nuget-packages/actions/workflows/sync-nuget.yml). Once
23-
it's complete, the package will be available in the Keyfactor Public GitHub Package Registry (GPR).
22+
### Running Locally
2423

25-
### Manually using dotnet CLI
24+
**Prerequisites:** Python 3.8+, `nuget` CLI (with Mono on Linux), `dotnet` CLI
25+
26+
1. Create a `.env` file in the repo root:
27+
28+
```bash
29+
export AZ_DEVOPS_PAT=<Azure DevOps PAT with package read permissions>
30+
export GITHUB_TOKEN=<GitHub PAT with write:packages permission>
31+
export GITHUB_ORG=keyfactor
32+
```
2633

27-
In order to add a nuget package to the Keyfactor Public GitHub Package Registry (GPR), you must download from the
28-
[DevOps Artifacts site](https://dev.azure.com/Keyfactor/Engineering/_artifacts/feed/KeyfactorPackages@Local)
29-
and then push it to the GPR using a Personal Access Token (PAT).
34+
2. Install dependencies and run:
35+
36+
```bash
37+
python -m venv venv && source venv/bin/activate
38+
pip install -e .
39+
source .env && python scripts/sync_nuget.py
40+
```
41+
42+
The script queries the GitHub Package Registry first and skips any versions already published.
43+
44+
### Manually using dotnet CLI
3045

31-
Steps:
32-
1. Create a new PAT with access with `write:packages` access
33-
2. Grant SSO to the PAT
34-
3. Run the following command to push the package to the GitHub Package Registry (GPR):
46+
1. Create a GitHub PAT with `write:packages` access and grant it SSO.
47+
2. Push the package:
3548

3649
```bash
3750
dotnet nuget push ./Your.Package.1.0.0.nupkg \

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ readme = "README.md"
1313
requires-python = ">=3.8"
1414
license = {file = "LICENSE"}
1515
dependencies = [
16-
"pyyaml>=6.0"
16+
"pyyaml>=6.0",
17+
"requests>=2.28"
1718
]
1819

1920
[project.optional-dependencies]

scripts/sync_nuget.py

Lines changed: 95 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import os
22
import shutil
33
import subprocess
4+
import tempfile
5+
import requests
46
import yaml
57

68
class NuGetSyncer:
79
def __init__(self):
810
self.NUGET_FEED_URL = "https://pkgs.dev.azure.com/Keyfactor/_packaging/KeyfactorPackages/nuget/v3/index.json"
911
self.GITHUB_NUGET_URL = "https://nuget.pkg.github.com/keyfactor/index.json"
1012
self.GITHUB_TOKEN = os.getenv("GH_NUGET_TOKEN", os.getenv("GITHUB_TOKEN"))
13+
self.AZ_DEVOPS_PAT = os.getenv("AZ_DEVOPS_PAT")
1114
self.TMP_DIR = "nupkgs"
1215
self.PACKAGES_YML = "packages.yml"
16+
self.GITHUB_NUGET_BASE = "https://nuget.pkg.github.com/keyfactor"
1317
self.allowed_packages = self.load_allowed_packages()
18+
self._github_versions_cache = {}
1419
os.makedirs(self.TMP_DIR, exist_ok=True)
1520

1621
def load_allowed_packages(self):
@@ -34,44 +39,84 @@ def get_all_packages_and_versions(self):
3439
return self.allowed_packages
3540
return {pkg: [] for pkg in self.allowed_packages}
3641

42+
def get_github_published_versions(self, name):
43+
"""Fetch the list of versions already published to GitHub Packages for a given package."""
44+
if name in self._github_versions_cache:
45+
return self._github_versions_cache[name]
46+
url = f"{self.GITHUB_NUGET_BASE}/download/{name.lower()}/index.json"
47+
try:
48+
resp = requests.get(url, auth=("token", self.GITHUB_TOKEN), timeout=15)
49+
if resp.status_code == 200:
50+
versions = set(resp.json().get("versions", []))
51+
else:
52+
versions = set()
53+
except Exception as e:
54+
print(f"Warning: could not fetch published versions for {name}: {e}")
55+
versions = set()
56+
self._github_versions_cache[name] = versions
57+
return versions
58+
3759
def download_package(self, name, version):
3860
filename = f"{name}.{version}.nupkg".replace("/", "_")
3961
filepath = os.path.join(self.TMP_DIR, filename)
4062
if os.path.exists(filepath):
4163
print(f"Already downloaded: {filename}")
4264
return filepath
4365
print(f"Downloading {name} {version} from NuGet v3 feed...")
44-
config_path = os.path.expanduser("~/.nuget/NuGet/NuGet.Config")
45-
cmd = [
46-
"nuget", "install", name,
47-
"-Source", self.NUGET_FEED_URL,
48-
"-Version", version,
49-
"-OutputDirectory", self.TMP_DIR,
50-
"-DirectDownload",
51-
"-ConfigFile", config_path
52-
]
53-
subprocess.run(cmd, check=True)
54-
# Find the downloaded .nupkg file
55-
pkg_dir = os.path.join(self.TMP_DIR, f"{name}.{version}")
56-
for file in os.listdir(pkg_dir):
57-
if file.endswith(".nupkg"):
58-
src = os.path.join(pkg_dir, file)
59-
os.rename(src, filepath)
60-
print(f"Downloaded: {filename}")
61-
break
62-
# Remove all non-nupkg files in the self.TMP_DIR
63-
files = os.listdir(self.TMP_DIR)
64-
for file in files:
65-
try:
66-
# Check if file is a directory
67-
full_path = os.path.join(self.TMP_DIR, file)
68-
if os.path.isdir(full_path):
69-
shutil.rmtree(full_path) # Recursively deletes directory and contents
70-
elif not file.endswith(".nupkg"):
71-
os.remove(full_path)
72-
except Exception as e:
73-
print(f"Failed to remove {file} in {self.TMP_DIR}. It may not be a directory or file.")
74-
print(e)
66+
tmp_config_path = None
67+
if self.AZ_DEVOPS_PAT:
68+
nuget_config = f"""<?xml version="1.0" encoding="utf-8"?>
69+
<configuration>
70+
<packageSources>
71+
<add key="KeyfactorPackages" value="{self.NUGET_FEED_URL}" />
72+
</packageSources>
73+
<packageSourceCredentials>
74+
<KeyfactorPackages>
75+
<add key="Username" value="any" />
76+
<add key="ClearTextPassword" value="{self.AZ_DEVOPS_PAT}" />
77+
</KeyfactorPackages>
78+
</packageSourceCredentials>
79+
</configuration>"""
80+
with tempfile.NamedTemporaryFile(mode='w', suffix='.config', delete=False) as tmp:
81+
tmp.write(nuget_config)
82+
tmp_config_path = tmp.name
83+
config_path = tmp_config_path
84+
else:
85+
config_path = os.path.expanduser("~/.nuget/NuGet/NuGet.Config")
86+
try:
87+
cmd = [
88+
"nuget", "install", name,
89+
"-Source", self.NUGET_FEED_URL,
90+
"-Version", version,
91+
"-OutputDirectory", self.TMP_DIR,
92+
"-DirectDownload",
93+
"-ConfigFile", config_path,
94+
]
95+
subprocess.run(cmd, check=True)
96+
# Find the downloaded .nupkg file
97+
pkg_dir = os.path.join(self.TMP_DIR, f"{name}.{version}")
98+
for file in os.listdir(pkg_dir):
99+
if file.endswith(".nupkg"):
100+
src = os.path.join(pkg_dir, file)
101+
os.rename(src, filepath)
102+
print(f"Downloaded: {filename}")
103+
break
104+
# Remove all non-nupkg files in the self.TMP_DIR
105+
files = os.listdir(self.TMP_DIR)
106+
for file in files:
107+
try:
108+
# Check if file is a directory
109+
full_path = os.path.join(self.TMP_DIR, file)
110+
if os.path.isdir(full_path):
111+
shutil.rmtree(full_path) # Recursively deletes directory and contents
112+
elif not file.endswith(".nupkg"):
113+
os.remove(full_path)
114+
except Exception as e:
115+
print(f"Failed to remove {file} in {self.TMP_DIR}. It may not be a directory or file.")
116+
print(e)
117+
finally:
118+
if tmp_config_path:
119+
os.unlink(tmp_config_path)
75120

76121
return filepath
77122

@@ -133,17 +178,34 @@ def sync_packages(self):
133178
return
134179
print(f"Will sync the following packages: {self.allowed_packages}")
135180
packages_and_versions = self.get_all_packages_and_versions()
181+
skipped = 0
182+
successful = 0
183+
failed = 0
136184
for pkg in packages_and_versions:
137185
pkg_name = pkg.get('name', pkg)
138186
versions = pkg.get('versions', [])
187+
published = self.get_github_published_versions(pkg_name)
139188
for version in versions:
189+
if version in published:
190+
print(f"Already published: {pkg_name} {version} — skipping")
191+
skipped += 1
192+
continue
140193
try:
141194
package_file = self.download_package(pkg_name, version)
142195
# Upload to GitHub after successful download
143196
if package_file and os.path.exists(package_file):
144-
self.upload_to_github(package_file)
197+
if self.upload_to_github(package_file):
198+
successful += 1
199+
else:
200+
failed += 1
145201
except Exception as e:
146-
print(f"Failed to download {pkg_name} {version}: {e}")
202+
print(f"Failed to sync {pkg_name} {version}: {e}")
203+
failed += 1
204+
205+
print("\nSync summary:")
206+
print(f" Uploaded: {successful}")
207+
print(f" Skipped: {skipped}")
208+
print(f" Failed: {failed}")
147209

148210
if __name__ == "__main__":
149211
syncer = NuGetSyncer()

0 commit comments

Comments
 (0)