Last updated
Jun 3, 2025
Import PowerShell Modules Directly from GitHub or GitLab without Installing
We know and love PowerShell modules. They do a lot of heavy lifting in the automation world! However, you might find yourself in a situation where you are not able to install a module on a system, but still want to make use of its functionality. For this purpose, I have Import-ModuleFromGitx
.
This is a function that takes in the URL of a public, cloud-hosted, GitHub or GitLab repository and imports it directly into your current PowerShell session.
Methodology
Parameters
There are two parameters for the function.
-Uri
is the URL of the repository you want to import. This required string can be piped in if you like, or specified directly.
-OutputDir
is the target directory to which the module will be downloaded. This string is optional, and will default to the present working directory if not specified.
URIs
The first problem I had is that the commonly used link for repositories is not a link that can be used to download the repository directly. To resolve this, I use a private helper function to parse the provided URI and construct the appropriate format that will let us download the repository. You know what they say: “if you have a problem and regex is the answer, you now have two problems.” I’m admittedly not the best at regex, but luckily
regex101.com exists and I ended up with this:
1
2
3
4
5
6
7
8
9
10
11
| function constructGitHubUri($Url) {
# Desired format: https://github.com/username/repositoryname/archive/refs/heads/main.zip
if ($Uri -match "^https://github\.com/([^/]+)/([^/]+)(?:/)?$") {
$user = $matches[1]
$repo = $matches[2] -replace "\.git$", ""
return "https://github.com/$user/$repo/archive/refs/heads/main.zip"
}
else {
exit 1
}
}
|
GitHub or GitLab?
I originally wrote this function specifically for GitHub, but realized it could be handy to make it support GitLab as well. Unfortunately, the URI format for GitHub and GitLab are different, so I have a similar private helper function for this one, too:
1
2
3
4
5
6
7
8
9
10
11
| function constructGitLabUri($Url) {
# Desired format: https://gitlab.com/workspacename/projectname/-/archive/main/projectname.zip
if ($Uri -match "^https://gitlab\.com/([^/]+)/([^/]+)(?:/)?$") {
$workspace = $matches[1]
$repo = $matches[2] -replace "\.git$", ""
return "https://gitlab.com/$workspace/$repo/-/archive/main/$repo-main.zip"
}
else {
exit 1
}
}
|
To determine which helper function to use, I use a switch
statement:
1
2
3
4
5
6
7
8
9
10
11
| switch ($Uri.Split('/')[2]) {
"github.com" {
$downloadUrl = constructGitHubUri -url $Uri
}
"gitlab.com" {
$downloadUrl = constructGitLabUri -url $Uri
}
default {
exit 1
}
}
|
Downloading the Repository
I then make a temporary directory to store the downloaded repository, verify that the target output directory exists, download the repository as a .zip archive, and extract it to the output directory.
The name of the staged .zip archive has a random GUID included to avoid any name collisions in the event that the same filename is already present. Additonally, the script checks to validate that the output directory exists and creates it if it does not.
1
2
3
4
5
6
7
8
9
10
11
| $tempZipPath = "$env:TEMP\repo_$(New-Guid).zip"
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir | Out-Null
}
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempZipPath -ErrorAction Stop
Expand-Archive -Path $tempZipPath -DestinationPath $OutputDir -Force -ErrorAction Stop
Remove-Item $tempZipPath -Force
|
The download and extraction is accomplished using simple, Windows-native functionality, such as Invoke-WebRequest
for downloading the .zip archive and Expand-Archive
for extracting it to the output directory. It was important to me that this function have no external dependencies (like needing Git installed on the machine).
Importing the Module
At this point, it’s import time! As you’d expect, I’m just using Import-Module
as usual, but targeting the .psm1
file in the downloaded and extracted repository:
1
| Import-Module -Name (Get-ChildItem -Path $OutputDir -Recurse -Filter *.psm1 | Select-Object -First 1).FullName
|
Using Get-ChildItem
with the -Recurse
and -Filter
parameters helps in easily finding the PowerShell module file that contains the functions to import. Because of this, the function does not yet support repositories with more complex structures or multiple modules. It grabs the first .psm1
file it finds–assuming that there is only one–which works for simple modules but might not work as expected if you in a nested or poly-module setup.
Of course, I also added some debug/verbose output and some light error handling as well but I figured I wouldn’t make this post longer than needed with that.
Considerations and Future Improvements
There are a few important caveats to know.
First, this function assumes that the main branch is named main
, which is the current Git default but not guaranteed. If the repository uses master
or a custom branch name, this will not work; I intend to add a -Branch
parameter to resolve this in the near future.
Security Note
Another thing to keep in mind is that the function doesn’t verify anything about the contents of the repo. There’s no signing check, no validation—so this is strictly a trust-based approach. You’ll want to be cautious and know what you’re pulling in.
Compliance Note
It is possible to use this function to get around restrictions in your environment that prevent you from installing modules directly from the PowerShell Gallery or other sources. You should comply with all regulations, company policies, licensing agreements, and other requirements/restrictions. Use responsibly and at your own risk.
Wrapping Up
Mostly, this was just a fun little exercise in PowerShell for me. But it could potentially be useful at some point, so I’m sharing it. If you find yourself in a similar situation, I hope this helps.
The full function is available as a
GitHub Gist as well as shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
| function Import-ModuleFromGitx {
<#
.SYNOPSIS
Imports a PowerShell module from a GitHub repository.
.DESCRIPTION
This function lets you import a PowerShell module directly from a GitHub repository without needing to install the module.
The module is downloaded from the specified GitHub repository URI, extracted, and imported into your current PowerShell session.
.PARAMETER Uri
The URI of the GitHub repository. This can be obtained by clicking the green "Code" button on the repository page and copying the HTTPS URL.
Type : String
Required : True
ValueFromPipeline : True
.PARAMETER OutputDir
The directory where the module will be downloaded and imported.
Type : String
Required : False
Default Value : $pwd
.EXAMPLE
# Import a PowerShell module from a GitHub or GitLab repository
Import-ModuleFromGitx -Uri "https://github.com/username/repository.git"
Import-ModuleFromGitx -Uri "https://gitlab.com/workspace/repository.git"
.EXAMPLE
# Import a PowerShell module from a GitHub or GitLab repository and specify an output directory other than the present working directory
Import-ModuleFromGitx -Uri "https://github.com/username/repository.git" -OutputDir "$env:USERPROFILE\Downloads"
Import-ModuleFromGitx -Uri "https://gitlab.com/workspace/repository.git" -OutputDir "$env:USERPROFILE\Downloads"
.NOTES
It is possible to use this function to get around restrictions in your environment that prevent you from installing modules directly from the PowerShell Gallery or other sources.
You should comply with all regulations, company policies, licensing agreements, and other requirements/restrictions. Use at your own risk.
Does NOT support privately hosted versions of GitLab.
.LINK
https://gist.github.com/griffeth-barker/30d781904c9a9579ce360303c4ed9e93
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]$Uri,
[Parameter(Mandatory = $false)]
[string]$OutputDir = "$pwd"
)
begin {
function constructGitHubUri($Url) {
if ($Uri -match "^https://github\.com/([^/]+)/([^/]+)(?:/)?$") {
$user = $matches[1]
$repo = $matches[2] -replace "\.git$", ""
return "https://github.com/$user/$repo/archive/refs/heads/main.zip"
}
else {
Write-Error "Invalid GitHub repository URL format. Should be 'https://github.com/username/repository.git' where username and repository are the GitHub valid user and repository names."
exit 1
}
}
function constructGitLabUri($Url) {
if ($Uri -match "^https://gitlab\.com/([^/]+)/([^/]+)(?:/)?$") {
$workspace = $matches[1]
$repo = $matches[2] -replace "\.git$", ""
return "https://gitlab.com/$workspace/$repo/-/archive/main/$repo-main.zip"
#https://gitlab.com/griffeth-barker/testing/-/archive/main/testing-main.zip
}
else {
Write-Error "Invalid GitLab repository URL format. Should be 'https://gitlab.com/workspace/repository.git' where username and repository are the GitLab valid user and repository names."
exit 1
}
}
}
process {
switch ($Uri.Split('/')[2]) {
"github.com" {
Write-Debug "Detected GitHub repository; using constructGitHubUri function"
$downloadUrl = constructGitHubUri -url $Uri
}
"gitlab.com" {
Write-Debug "Detected GitLab repository; using constructGitLabUri function"
$downloadUrl = constructGitLabUri -url $Uri
}
default {
Write-Error "Unsupported repository type. Only GitHub and GitLab repositories are supported at this time."
exit 1
}
}
try {
Write-Verbose "Creating temporary directory for download..."
Write-Debug "Creating '$env:TEMP\repo_$(New-Guid).zip'"
$tempZipPath = "$env:TEMP\repo_$(New-Guid).zip"
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir | Out-Null
}
Write-Verbose "Downloading repository..."
Write-Debug "Using URI: $Uri"
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempZipPath -ErrorAction Stop
Write-Debug "File saved to $tempZipPath"
Write-Debug "Extracting to $OutputDir"
Expand-Archive -Path $tempZipPath -DestinationPath $OutputDir -Force -ErrorAction Stop
Write-Debug "Removing $tempZipPath"
Remove-Item $tempZipPath -Force
Write-Verbose "Importing module..."
Write-Debug "Importing module from $OutputDir"
Import-Module -Name (Get-ChildItem -Path $OutputDir -Recurse -Filter *.psm1 | Select-Object -First 1).FullName
}
catch {
Write-Error $_.Exception.Message
}
}
end {
}
}
|