Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support publishing with dependencies #729

Merged
merged 10 commits into from
Feb 11, 2025
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #469 Added ability to include an `IPMVersion` in `SystemRequirement` of module.xml
- #530 Added a `CustomPhase` attribute to `<Invoke>` that doesn't require a corresponding %method in lifecycle class.
- #582 Added functionality to optionally see time of last update and server version of each package
- #609 Added support for `-export-deps` when running the "Package" phase of lifecycle
- #609,#729 Added support for `-export-deps` when running the "Package" phase (and, consequently, the "Publish" phase) of lifecycle
- #541 Added support for ORAS repository
- #702 Added a new lifecycle phase `Initialize` which is used for preload
- #702 Added a `<CPF/>` resource, which can be used for CPF merge before/after a specified lifecycle phase or in a custom lifecycle phase.
Expand All @@ -30,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #696: Fix a bug that caused error status to be ignored when publishing a module.
- #700: Fix a bug due to incompatible conventions between SemVer and OCI tags
- #669: Work with a wider variety of ORAS repos (removes _catalog call)
- #726: Fixed a bug where loading a tarball doesn't install dependencies from `.modules` subfolder even when it's available
- #726,#729: Fixed a bug where install/loading a tarball doesn't install dependencies from `.modules` subfolder even when it's available
- #731: Issue upgrading from v0.9.x due to refactor of repo classes
- #718: Upload zpm.xml (without the version) as an artifact to provide a more stable URL to latest release artifact on GitHub

Expand Down
86 changes: 86 additions & 0 deletions src/cls/IPM/General/TempLocalRepoManager.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
Include %IPM.Formatting

Class %IPM.General.TempLocalRepoManager Extends %RegisteredObject
{

Property Root As %String;

Property Repo As %IPM.Repo.Definition;

/// Creates the repository. If anything goes wrong, it will throw an error after cleaning up the repo.
/// Purposedly private and not a classmethod, so that we can only call it through %OnNew.
/// This encourages user to perform clean-up using the return instance from %OnNew.
Method Create(useFirst As %Boolean) [ Internal, Private ]
{
Set count = 0
For {
Set repoName = "ipm-temp-modules-" _ $Increment(count)
If '##class(%IPM.Repo.Definition).ServerDefinitionKeyExists(repoName) {
Quit
}
}
Set ..Repo = ##class(%IPM.Repo.Filesystem.Definition).%New()
Set ..Repo.Name = repoName
Set ..Repo.Root = ..Root
Set ..Repo.Snapshots = 1
Set ..Repo.Prereleases = 1
// Make sure this is the first/last repo to be found by SQL query in %IPM.Repo.Manager:SearchRepositoriesForModule
Set ..Repo.OverriddenSortOrder = $SELECT(useFirst:-1000 ,1:1000)

$$$ThrowOnError(..Repo.BuildCache(1,1,1))
}

ClassMethod SkipCreate(location As %String) As %Boolean [ Internal ]
{
If (location = "") || ('##class(%File).DirectoryExists(location)) {
Return 1
}

/// There is a unique index on the "Root" column, so skip creating the repo if it already exists (e.g., setup by another thread)
Set query = "SELECT COUNT(*) As Total FROM %IPM_Repo_Filesystem.Definition WHERE Root = ?"
Set rs = ##class(%SQL.Statement).%ExecDirect(, query, location)
$$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message)
If rs.%Next() && (rs.%Get("Total") > 0) {
Return 1
}
Return 0
}

Method %OnNew(location As %String, useFirst As %Boolean = 0) As %Status
{
/// If the location is empty or already covered by another repo, skip creating the repo
/// This will still create an intance of this class, but the cleanup will be a no-op
Try {
If ..SkipCreate($Get(location)) {
Return $$$OK
}
} Catch ex {
Return ex.AsStatus()
}

Set ..Root = $Get(location)
Try {
Do ..Create(useFirst)
} Catch ex {
Return $$$ADDSC(ex.AsStatus(), ..CleanUp())
}

Return $$$OK
}

Method CleanUp() As %Status
{
If ('$IsObject(..Repo)) || (..Repo.%Id() = "") {
Quit $$$OK
}

Set sc = ..Repo.%DeleteId(..Repo.%Id())
If $$$ISERR(sc) {
Set msg = $$$FormatText("Failed to clean up repository '%1'. You may need to manually delete it using 'repo -delete -n %1'", ..Repo.Name)
Set msg = $$$FormattedLine($$$Red, msg)
Write !, msg
}
Quit sc
}

}
24 changes: 4 additions & 20 deletions src/cls/IPM/Main.cls
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Currently, there may only be one of these per namespace.
<modifier name="dev" dataAlias="DeveloperMode" dataValue="1" description="Sets the DeveloperMode flag for the module's lifecycle. Key consequences of this are that ^Sources will be configured for resources in the module, and installer methods will be called with the dev mode flag set." />
<modifier name="quiet" aliases="q" dataAlias="Verbose" dataValue="0" description="Produces minimal output from the command." />
<modifier name="verbose" aliases="v" dataAlias="Verbose" dataValue="1" description="Produces verbose output from the command." />
<modifier name="export-deps" value="true" valueList="0,1" dataAlias="ExportDependencies" description="If specified, controls whether dependencies are exported. If omitted, defaults to the value of the #EXPORTDEPENDENCIES in lifecycle class. This modifier is only used in &quot;Package&quot; lifecycle." />
<modifier name="export-deps" value="true" valueList="0,1" dataAlias="ExportDependencies" description="If specified, controls whether dependencies are exported. If omitted, defaults to the value of the #EXPORTDEPENDENCIES in lifecycle class. This modifier is only used in &quot;Package&quot; and &quot;Publish&quot; lifecycles." />

</command>

Expand Down Expand Up @@ -166,6 +166,7 @@ This command is an alias for `module-action module-name publish`
<modifier name="verbose" aliases="v" dataAlias="Verbose" dataValue="1" description="Produces verbose output from the command." />
<modifier name="repo" aliases="r" dataAlias="PublishTo" description="Namespace-unique name for the module to publish to (if deployment is enabled)" />
<modifier name="use-external-name" aliases="use-ext" dataAlias="UseExternalName" dataValue="1" description="Publish the package under the &lt;ExternalName&gt; of the package. If ExternalName is unspecified or illegal, an error will be thrown."/>
<modifier name="export-deps" value="true" valueList="0,1" dataAlias="ExportDependencies" description="If specified, controls whether dependencies are published. If omitted, defaults to the value of the #EXPORTDEPENDENCIES in lifecycle class." />
</command>

<command name="makedeployed">
Expand Down Expand Up @@ -2109,26 +2110,9 @@ ClassMethod Load(ByRef pCommandInfo) [ Internal ]
// It's easier to configure a temporary repository than to handle this case in the dependency resolution code.
Set tTargetDirectory = $Get(tTargetDirectory, tDirectoryName)
Set dotModules = ##class(%File).NormalizeDirectory(".modules", tTargetDirectory)
If (dotModules '= "") && ##class(%File).DirectoryExists(dotModules) {
Set count = 0
For {
Set repoName = "ipm-temp-dot-modules-" _ $Increment(count)
If '##class(%IPM.Repo.Definition).ServerDefinitionKeyExists(repoName) {
Quit
}
}
Set tempRepo = ##class(%IPM.Repo.Filesystem.Definition).%New()
Set tempRepo.Name = repoName
Set tempRepo.Root = dotModules
Set tempRepo.Snapshots = 1
Set tempRepo.Prereleases = 1
Set tempRepo.OverriddenSortOrder = -1000 // Make sure this is the first repo to be found by SQL query in %IPM.Repo.Manager:SearchRepositoriesForModule
$$$ThrowOnError(tempRepo.BuildCache(1,1,1))
}
Set tmpRepoMgr = ##class(%IPM.General.TempLocalRepoManager).%New(dotModules, 1)
Set tSC = ##class(%IPM.Utils.Module).LoadNewModule(tTargetDirectory, .tParams)
If $Data(tempRepo) # 2 {
Set tSC = $$$ADDSC(tSC, tempRepo.%DeleteId(tempRepo.%Id()))
}
Set tSC = $$$ADDSC(tSC, tmpRepoMgr.CleanUp())
$$$ThrowOnError(tSC)
}

Expand Down
6 changes: 6 additions & 0 deletions src/cls/IPM/Utils/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,10 @@ ClassMethod LoadModuleReference(pServerName As %String, pModuleName As %String,
// This doesn't make sense if we're loading the dependency from a remote server.
Set tDeveloperMode = 1
}
Set dotModules = ##class(%File).NormalizeDirectory(".modules", tDirectory)
Set tmpRepoMgr = ##class(%IPM.General.TempLocalRepoManager).%New(dotModules, 0)
Set tSC = ..LoadModuleFromDirectory(tDirectory,.pParams,tDeveloperMode,pServerName)
Set tSC = $$$ADDSC(tSC, tmpRepoMgr.CleanUp())
} Else {
Set tAsArchive = -1
Set tPayload = tClient.GetModule(tModRef, .tAsArchive)
Expand Down Expand Up @@ -405,7 +408,10 @@ ClassMethod LoadModuleFromArchive(pModuleName As %String, pModuleVersion As %Str
Write:tVerbose !,tOutput(i)
}

Set dotModules = ##class(%File).NormalizeDirectory(".modules", tTargetDirectory)
Set tmpRepoMgr = ##class(%IPM.General.TempLocalRepoManager).%New(dotModules, 0)
Set tSC = ..LoadModuleFromDirectory(tTargetDirectory, .pParams, , pRepository)
Set tSC = $$$ADDSC(tSC, tmpRepoMgr.CleanUp())
If $$$ISERR(tSC) {
Quit
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Class Test.PM.Integration.PublishModuleWithDeps Extends Test.PM.Integration.Base
{

Method TestPublishWithDeps() As %Status
{
Set moduleDir = ..GetModuleDir()

Set sc = ##class(%IPM.Main).Shell("repo -fs -name localrepo -path " _ moduleDir)
Do $$$AssertStatusOK(sc, "Successfully configured local filesystem repo")

Set sc = ##class(%IPM.Main).Shell("install -v localrepo/publish-with-deps")
Do $$$AssertStatusOK(sc, "Successfully installed module from local filesystem repo")

Set sc = ##class(%IPM.Main).Shell("repo -delete -n localrepo")
Do $$$AssertStatusOK(sc, "Successfully deleted local filesystem repo")

// This test should work in the CI environment, where the registry is already set up
// To make it pass locally, you need to manually set up the registry at the following URL and set the username and password accordingly
Set sc = ##class(%IPM.Main).Shell("repo -r -name customrepo -url http://registry:52773/registry -username admin -password SYS")
Do $$$AssertStatusOK(sc, "Successfully set up custom repo")

Set sc = ##class(%IPM.Main).Shell("publish publish-with-deps -r customrepo -v -export-deps 1")
Do $$$AssertStatusOK(sc, "Successfully published module with dependencies")

Set sc = ##class(%IPM.Main).Shell("uninstall publish-with-deps-dep")
Do $$$AssertStatusOK(sc, "Successfully uninstalled dependency")

Set sc = ##class(%IPM.Main).Shell("uninstall publish-with-deps")
Do $$$AssertStatusOK(sc, "Successfully uninstalled module")

Set sc = ##class(%IPM.Main).Shell("install -v customrepo/publish-with-deps")
Do $$$AssertStatusOK(sc, "Successfully installed module from custom repo")

Set sc = ##class(%IPM.Main).Shell("repo -delete -n customrepo")
Do $$$AssertStatusOK(sc, "Successfully deleted local registry")

Set sc = ##class(%IPM.Main).Shell("uninstall publish-with-deps-dep")
Do $$$AssertStatusOK(sc, "Successfully uninstalled dependency again")

Set sc = ##class(%IPM.Main).Shell("uninstall publish-with-deps")
Do $$$AssertStatusOK(sc, "Successfully uninstalled module again")

Quit $$$OK
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="publish-with-deps-dep.ZPM">
<Module>
<Name>publish-with-deps-dep</Name>
<Version>0.0.1</Version>
<Packaging>module</Packaging>
</Module>
</Document>
</Export>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="publish-with-deps.ZPM">
<Module>
<Name>publish-with-deps</Name>
<Version>0.0.1</Version>
<Packaging>module</Packaging>
<Dependencies>
<ModuleReference>
<Name>publish-with-deps-dep</Name>
<Version>0.0.1</Version>
</ModuleReference>
</Dependencies>
</Module>
</Document>
</Export>