Skip to content

Commit

Permalink
Add the ability to upload using a folder and not just Git
Browse files Browse the repository at this point in the history
  • Loading branch information
OliverRC committed Jun 28, 2024
1 parent bc2fe9c commit c1b3904
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 104 deletions.
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ If you are using it on your local development or would just like to have it inst

Once installed you can run the tool from the command line.

### Tags (e.g Github Release)
Default which uses mode = Tags (e.g GitHub Releases)

kickflip deploy --hostname <ftp-hostname> --port <ftp-port (24)> --username <ftp-username> --password <ftp-password>
Expand All @@ -30,12 +31,28 @@ Or Tags

kickflip deploy --mode Tags --hostname <ftp-hostname> --port <ftp-port (24)> --username <ftp-username> --password <ftp-password>

### GitHubMergePr

This tries to work out the changes between two PR merges. Useful for rapid deployment scenarios where PR's are used and you don't need to bundle multiple merges together.

kickflip deploy --mode GitHubMergePr --hostname <ftp-hostname> --port <ftp-port (24)> --username <ftp-username> --password <ftp-password>

### Folder

You may want to statically upload the contents of a folder. For example maybe there is some build assets produced on on your CI/CD server.

kickflip deploy --mode Folder --deployment-path /public_html --hostname <ftp-hostname> --port <ftp-port (24)> --username <ftp-username> --password <ftp-password> --folder <folder-path>

**Note**: `--deployment-path` defaults to `/` however it is strongly recommended to set this to ensure you know where the files are going.

**Caveat**: To ignore files the `.kickflipignore` file must be present IN the folder.

## Github Actions

Github Actions `actions/checkout@v3` by default performs a shallow clone of the repo. In order for kickflip to work out all the changes it requires that a full clone be made. This can be achieve by:
Github Actions `actions/checkout@v4` by default performs a shallow clone of the repo. In order for kickflip to work out all the changes it requires that a full clone be made. This can be achieve by:

```yaml
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0 # avoid shallow clone so kickflip can do its work.
```
Expand Down
3 changes: 2 additions & 1 deletion src/kickflip/Enums/FindMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ namespace kickflip.Enums;
public enum FindMode
{
Tags,
GitHubMergePR
GitHubMergePR,
Folder
}
7 changes: 7 additions & 0 deletions src/kickflip/Enums/Source.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace kickflip.Enums;

public enum Source
{
Git,
Folder
}
1 change: 1 addition & 0 deletions src/kickflip/Models/DeploymentAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public enum DeploymentAction
{
Add,
Modify,
AddOrModify,
Delete,
Ignore
}
14 changes: 3 additions & 11 deletions src/kickflip/Models/DeploymentChange.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
namespace kickflip.Models;
using kickflip.Enums;

public class DeploymentChange
{
public DeploymentChange(DeploymentAction action, string path)
{
Action = action;
Path = path;
}
namespace kickflip.Models;

public DeploymentAction Action { get; }
public string Path { get; }
}
public record DeploymentChange(DeploymentAction Action, Source Source, string Path, string DeploymentPath);
113 changes: 67 additions & 46 deletions src/kickflip/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ static async Task<int> Main(string[] args)

static Command DeployCommand()
{
var localPathArgument = new Argument<string?>
(name: "local-path",
var pathArgument = new Argument<string?>
(name: "path",
description:
"Local path to the folder containing the git repository. The local path is an absolute path.",
"Path to the folder containing the git repository for Git based deployment modes or the folder to use in folder deployment. The path is an absolute path.",
getDefaultValue: Directory.GetCurrentDirectory);

var findModeOption = new Option<FindMode>(
name: "--mode",
description:
"The find mode to use when trying to determine the starting point to compare changes too HEAD with.",
getDefaultValue: () => FindMode.Tags);

var deploymentPathOption = new Option<string?>
(name: "--deployment-path",
description:
Expand All @@ -57,7 +57,7 @@ static Command DeployCommand()
name: "--password",
description: "Authentication password for the remote server"
) {IsRequired = true};

var dryRunOption = new Option<bool>(
name: "--dry",
description:
Expand All @@ -66,66 +66,42 @@ static Command DeployCommand()

var deployCommand =
new Command("deploy", "Kick your git changes and flip them over to that remote server!");
deployCommand.AddArgument(localPathArgument);

deployCommand.AddArgument(pathArgument);
deployCommand.AddOption(findModeOption);
deployCommand.AddOption(deploymentPathOption);
deployCommand.AddOption(hostnameOption);
deployCommand.AddOption(portOption);
deployCommand.AddOption(usernameOption);
deployCommand.AddOption(passwordOption);
deployCommand.AddOption(dryRunOption);
deployCommand.SetHandler(HandleDeployment!, localPathArgument, findModeOption, deploymentPathOption, hostnameOption,

deployCommand.SetHandler(HandleDeployment!, pathArgument, findModeOption, deploymentPathOption, hostnameOption,
portOption, usernameOption, passwordOption, dryRunOption);

return deployCommand;
}

static Task<int> HandleDeployment(
string localPath,
FindMode findMode,
string deploymentPath,
string hostname,
int port,
string username,
string password,
bool isDryRun)
{
var ignoreService = new IgnoreService(localPath);
var gitService = new GitService(ignoreService);
var outputService = new OutputService();
var deploymentService = new SftpDeploymentService(hostname, port, username, password, deploymentPath);

var changes = gitService.GetChanges(localPath, findMode);

Console.WriteLine(outputService.GetChangesConsole(changes));

var result = deploymentService.DeployChanges(localPath, changes, isDryRun);
if (!result)
{
Console.WriteLine("Deployment failure. Please check the logs for more information.");
return Task.FromResult((int) ExitCodes.FailedWithErrors);
}

Console.WriteLine("Deployment successful!");
return Task.FromResult((int) ExitCodes.Success);
}

static Command GithubCommand()
{
var localPathArgument = new Argument<string?>
(name: "local-path",
description:
"Local path to the folder containing the git repository. The local path is an absolute path.",
getDefaultValue: Directory.GetCurrentDirectory);

var findModeOption = new Option<FindMode>(
name: "--mode",
description:
"The find mode to use when trying to determine the starting point to compare changes too HEAD with.",
getDefaultValue: () => FindMode.Tags);


var deploymentPathOption = new Option<string?>
(name: "--deployment-path",
description:
"Deployment path on the remote server to deploy to. The deployment path is relative to the root of the user account on the remote server.",
getDefaultValue: () => "/");

var repositoryOption = new Option<string?>(
name: "--repo",
description: "The repository where the PR resides. Should be in the format of <owner>/<repository>.",
Expand Down Expand Up @@ -170,10 +146,11 @@ static Command GithubCommand()
pullRequestCommand.AddAlias("pr");
pullRequestCommand.AddArgument(localPathArgument);
pullRequestCommand.AddOption(findModeOption);
pullRequestCommand.AddOption(deploymentPathOption);
pullRequestCommand.AddOption(repositoryOption);
pullRequestCommand.AddOption(refOption);
pullRequestCommand.AddOption(tokenOption);
pullRequestCommand.SetHandler(HandleGithubPullRequest!, localPathArgument, findModeOption, repositoryOption, refOption, tokenOption);
pullRequestCommand.SetHandler(HandleGithubPullRequest!, localPathArgument, findModeOption, deploymentPathOption, repositoryOption, refOption, tokenOption);

var githubCommand = new Command("github",
"Integration with github to allow kickflip to work in your existing Github workflow.");
Expand All @@ -182,25 +159,69 @@ static Command GithubCommand()
return githubCommand;
}

private static async Task<int> HandleGithubPullRequest(string localPath, FindMode findMode, string repository, string pullRequestReference, string token)
static Task<int> HandleDeployment(
string localPath,
FindMode findMode,
string deploymentPath,
string hostname,
int port,
string username,
string password,
bool isDryRun)
{
var ignoreService = new IgnoreService(localPath);
var gitService = new GitService(ignoreService);
var fileSystemService = new FileSystemService(ignoreService);
var outputService = new OutputService();
var deploymentService = new SftpDeploymentService(hostname, port, username, password, deploymentPath);

var changes = findMode switch
{
FindMode.Tags or FindMode.GitHubMergePR => gitService.GetChanges(localPath, deploymentPath, findMode),
FindMode.Folder => fileSystemService.GetChanges(localPath, deploymentPath),
_ => throw new ArgumentOutOfRangeException(nameof(findMode), findMode, null)
};

Console.WriteLine(outputService.GetChangesConsole(changes));

var result = deploymentService.DeployChanges(localPath, changes, isDryRun);
if (!result)
{
Console.WriteLine("Deployment failure. Please check the logs for more information.");
return Task.FromResult((int) ExitCodes.FailedWithErrors);
}

Console.WriteLine("Deployment successful!");
return Task.FromResult((int) ExitCodes.Success);
}

private static async Task<int> HandleGithubPullRequest(string localPath, FindMode findMode, string deploymentPath, string repository, string pullRequestReference,
string token)
{
var ignoreService = new IgnoreService(localPath);
var gitService = new GitService(ignoreService);
var fileSystemService = new FileSystemService(ignoreService);
var gitHubService = new GithubService(token);
var outputService = new OutputService();

var changes = findMode switch
{
FindMode.Tags or FindMode.GitHubMergePR => gitService.GetChanges(localPath, deploymentPath, findMode),
FindMode.Folder => fileSystemService.GetChanges(localPath, deploymentPath),
_ => throw new ArgumentOutOfRangeException(nameof(findMode), findMode, null)
};

var changes = gitService.GetChanges(localPath, findMode);
var comments = outputService.GetChangesMarkdown(changes);
var result = await gitHubService.PullRequestCommentChanges(repository, pullRequestReference, comments);

if (!result)
{
Console.WriteLine("Github Pull Request Comment Failure. Please check the logs for more information.");
return (int) ExitCodes.FailedWithErrors;
}

Console.WriteLine("Github Pull Request Comment Successful!");
return (int)ExitCodes.Success;
return (int) ExitCodes.Success;
}
}
}
39 changes: 39 additions & 0 deletions src/kickflip/Services/FileSystemService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using kickflip.Enums;

namespace kickflip.Services;

public class FileSystemService(IgnoreService ignoreService)
{
/// <summary>
/// Gets all the files in the specified directory (minus any ignored files) and returns them as a list of deployment changes.
/// </summary>
public List<DeploymentChange> GetChanges(string localPath, string deploymentPath)
{
var changes = new List<DeploymentChange>();

// Check path is valid and a directory
if (!Directory.Exists(localPath))
{
throw new DirectoryNotFoundException($"The specified path does not exist: {localPath}");
}

// Get all files in the directory
var files = Directory.GetFiles(localPath, "*", SearchOption.AllDirectories);

// Filter out ignored files, add the rest to the list of changes
foreach (string file in files)
{
var relativePath = file.Replace(localPath, string.Empty).TrimStart(Path.DirectorySeparatorChar);
if (ignoreService.IsIgnored(relativePath))
{
changes.Add(new DeploymentChange(DeploymentAction.Ignore, Source.Folder, relativePath, ""));
continue;
}

var deploymentFilePath = Utilities.UrlCombine(deploymentPath, relativePath);
changes.Add(new DeploymentChange(DeploymentAction.AddOrModify, Source.Folder, relativePath, deploymentFilePath));
}

return changes;
}
}
30 changes: 12 additions & 18 deletions src/kickflip/Services/GitService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,9 @@

namespace kickflip.Services;

public class GitService
public class GitService(IgnoreService ignoreService)
{
private readonly IgnoreService _ignoreService;

public GitService(IgnoreService ignoreService)
{
_ignoreService = ignoreService;
}

public List<DeploymentChange> GetChanges(string path, FindMode findMode)
public List<DeploymentChange> GetChanges(string path, string deploymentPath, FindMode findMode)
{
// string path = Environment.CurrentDirectory;
using var repo = new Repository(path);
Expand Down Expand Up @@ -59,36 +52,37 @@ public List<DeploymentChange> GetChanges(string path, FindMode findMode)
Console.WriteLine($" - {ChangeKind.Added} {change.Path}");
}

deploymentChanges.AddRange(ToDeploymentChanges(change));
deploymentChanges.AddRange(ToDeploymentChanges(change, deploymentPath));
}

return deploymentChanges;
}

private IEnumerable<DeploymentChange> ToDeploymentChanges(TreeEntryChanges change)
private IEnumerable<DeploymentChange> ToDeploymentChanges(TreeEntryChanges change, string deploymentPath)
{
var changes = new List<DeploymentChange>();

if (_ignoreService.IsIgnored(change.Path))
if (ignoreService.IsIgnored(change.Path))
{
changes.Add(new DeploymentChange(DeploymentAction.Ignore, change.Path));
changes.Add(new DeploymentChange(DeploymentAction.Ignore, Source.Git, change.Path, ""));
return changes;
}

var deploymentPathWithFile = Path.Combine(deploymentPath, change.Path);
switch (change.Status)
{
case ChangeKind.Added:
changes.Add(new DeploymentChange(DeploymentAction.Add, change.Path));
changes.Add(new DeploymentChange(DeploymentAction.Add, Source.Git, change.Path, deploymentPathWithFile));
break;
case ChangeKind.Deleted:
changes.Add(new DeploymentChange(DeploymentAction.Delete, change.Path));
changes.Add(new DeploymentChange(DeploymentAction.Delete, Source.Git, change.Path, deploymentPathWithFile));
break;
case ChangeKind.Modified:
changes.Add(new DeploymentChange(DeploymentAction.Modify, change.Path));
changes.Add(new DeploymentChange(DeploymentAction.Modify, Source.Git, change.Path, deploymentPathWithFile));
break;
case ChangeKind.Renamed:
changes.Add(new DeploymentChange(DeploymentAction.Add, change.Path));
changes.Add(new DeploymentChange(DeploymentAction.Delete, change.OldPath));
changes.Add(new DeploymentChange(DeploymentAction.Add, Source.Git, change.Path, deploymentPathWithFile));
changes.Add(new DeploymentChange(DeploymentAction.Delete, Source.Git, change.OldPath, deploymentPathWithFile));
break;
default:
throw new ArgumentOutOfRangeException(nameof(change.Status), change.Status, "Currently only Added, Deleted, Modified and Renamed are supported");
Expand Down
1 change: 1 addition & 0 deletions src/kickflip/Services/IgnoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class IgnoreService
public IgnoreService(string localPath)
{
_matcher = new Matcher();
_matcher.AddInclude("**/*.kickflipignore");

var ignoreFile = Path.Combine(localPath, ".kickflipignore");
if (!File.Exists(ignoreFile))
Expand Down
Loading

0 comments on commit c1b3904

Please sign in to comment.