Compare commits

27 Commits
3.0.0 ... 3.1.2

Author SHA1 Message Date
Sam Kirkland
6647c2f8ad Merge pull request #346 from SamKirkland/v3.1.2-patch
v3.1.2-patch
2022-12-15 09:39:43 -06:00
Sam Kirkland
41575a636c v3.1.2-patch 2022-12-15 02:45:45 -06:00
SamKirkland
4158862244 v3.1.2
node v16
2022-12-02 11:14:15 -06:00
Sam Kirkland
559ddbb4df Update Dockerfile 2021-08-16 11:53:47 -05:00
SamKirkland
da0d77ff39 v3.1.1
Bug fix: argument escaping for spaces within passwords
2020-05-17 17:28:14 -05:00
SamKirkland
1af692f7d5 Updating linux testing instructions 2020-05-17 17:25:20 -05:00
SamKirkland
ae5262e007 using act for local debugging
Added act library for local debugging
Attempting to use exec argument escaping
2020-05-17 16:57:25 -05:00
Sam Kirkland
b698c49eac Merge pull request #72 from Helias/patch-1
Add Linux istructions
2020-05-07 00:25:44 -05:00
Stefano Borzì
ccf756b42e Add Linux istructions 2020-05-06 18:21:47 +02:00
SamKirkland
f68449776c v3.1.0
Official release of known-hosts
2020-05-05 00:58:08 -05:00
SamKirkland
a54cf4c5e3 Closes #60
Due to node issue using exit code 0 for exceptions thrown in async methods.
Fixed by moving getUserArguments inside try block.
2020-05-02 02:14:10 -05:00
SamKirkland
c42c8e46fb Updating ReadMe FAQ Section 2020-05-02 00:24:55 -05:00
SamKirkland
c926b9df00 Library & test updates
- Updated NPM packages
- Updated tests to point to correct branch (oops)
2020-05-01 23:07:16 -05:00
Sam Kirkland
3f7edaa478 Merge pull request #58 from SamKirkland/dependabot/npm_and_yarn/acorn-5.7.4
Bump acorn from 5.7.3 to 5.7.4
2020-04-04 15:50:05 -05:00
dependabot[bot]
e39df43686 Bump acorn from 5.7.3 to 5.7.4
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-04-04 20:46:45 +00:00
Sam Kirkland
586c7242ef Merge pull request #55 from apokalyptik/master
known_hosts support
2020-04-04 15:45:04 -05:00
Sam Kirkland
97865be6a0 Update README.md
Removed incorrect information about known_hosts expiring
2020-04-04 15:43:55 -05:00
Sam Kirkland
b890f82a46 Update README.md
I tested this out on my host, added commands for windows 10 and moved the docs around a bit
2020-04-02 00:47:59 -05:00
apokalyptik
c9f0bcd878 Update README.md 2020-03-31 10:10:56 -07:00
apokalyptik
481f9001ff work on formatting 2020-03-31 10:10:10 -07:00
Demitrious Kelly
4938a6057e README update, and rebuild of index.js 2020-03-31 10:00:49 -07:00
Sam Kirkland
af948b8060 Update main.ts
Code Cleanup changes:
- Pulled out this PR into a new function `configureHost`
- Placed args.knownHosts empty at the top of the function, this is known as "fail fast" and helps make the code more readable. Instead of wrapping our code in if/else blocks.
- Converted fs.writeFile to a awaitable function to make it more readable
- Pulled out sshFolder into a const
- Removed `console.log('Wrote ' + process.env['HOME'] + '/.ssh/known_hosts');` logging. Probably not needed because we log ` Configured known_hosts` just 2 lines later
- Small formatting changes

Bug fixes:
- Converted fs.writeFile to a awaitable function, previously the code continued to execute so this could open up a race condition. Also it's more readable :)
 - Race condition example: Previously we asked node to write the file `known_hosts` but we never verified the IO operation completed before modifying the files permission, or deploying the site. If this IO operation wasn't immediate the next function would throw or the deploy would error out.
- Any exception within the new method was swallowed because we had a catch without a throw. In this case let's end the program run instead of attempting to deploy
-
2020-03-30 23:20:38 -05:00
Demitrious Kelly
db3b78d8e7 more spaghetti at the wall [2] 2020-03-25 15:28:33 -07:00
Demitrious Kelly
034d210969 more spaghetti at the wall 2020-03-25 15:15:12 -07:00
Demitrious Kelly
2863e02eda home? 2020-03-25 15:03:56 -07:00
Demitrious Kelly
9315d47124 attempt to create known_hosts 2020-03-25 14:52:17 -07:00
Sam Kirkland
98039f1fbe Added notes about git-ftp-include 2020-02-20 13:16:57 -06:00
12 changed files with 2739 additions and 2101 deletions

View File

@@ -5,11 +5,11 @@ jobs:
name: FTP-Deploy-Action name: FTP-Deploy-Action
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v2.1.0
with: with:
fetch-depth: 2 fetch-depth: 2
- name: FTP-Deploy-Action - name: FTP-Deploy-Action
uses: SamKirkland/FTP-Deploy-Action-Typescript@master uses: SamKirkland/FTP-Deploy-Action@master
with: with:
ftp-server: ftp://ftp.samkirkland.com/ ftp-server: ftp://ftp.samkirkland.com/
ftp-username: ${{ secrets.ftp_username }} ftp-username: ${{ secrets.ftp_username }}

View File

@@ -5,11 +5,11 @@ jobs:
name: FTP-Deploy-Action name: FTP-Deploy-Action
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v2.1.0
with: with:
fetch-depth: 2 fetch-depth: 2
- name: FTP-Deploy-Action - name: FTP-Deploy-Action
uses: SamKirkland/FTP-Deploy-Action-Typescript@master uses: SamKirkland/FTP-Deploy-Action@master
with: with:
# deploy to a folder named "sftp-deploy-test.samkirkland.com" on my server # deploy to a folder named "sftp-deploy-test.samkirkland.com" on my server
ftp-server: sftp://ftp.samkirkland.com:7822/home/samkirkland/sftp-deploy-test.samkirkland.com/ ftp-server: sftp://ftp.samkirkland.com:7822/home/samkirkland/sftp-deploy-test.samkirkland.com/

243
README.md
View File

@@ -4,11 +4,13 @@
Automate deploying websites and more with this GitHub action Automate deploying websites and more with this GitHub action
![Action](images/action-preview.gif)
![Test FTP Deploy](https://github.com/SamKirkland/FTP-Deploy-Action/workflows/Test%20FTP%20Deploy/badge.svg) ![Test SFTP Deploy](https://github.com/SamKirkland/FTP-Deploy-Action/workflows/Test%20SFTP%20Deploy/badge.svg) ![Test FTP Deploy](https://github.com/SamKirkland/FTP-Deploy-Action/workflows/Test%20FTP%20Deploy/badge.svg) ![Test SFTP Deploy](https://github.com/SamKirkland/FTP-Deploy-Action/workflows/Test%20SFTP%20Deploy/badge.svg)
### Usage Example (Your_Project/.github/workflows/main.yml) ---
### Usage Example
Place the following in `Your_Project/.github/workflows/main.yml`
```yml ```yml
on: push on: push
name: Publish Website name: Publish Website
@@ -17,18 +19,21 @@ jobs:
name: FTP-Deploy-Action name: FTP-Deploy-Action
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v2.1.0
with: with:
fetch-depth: 2 fetch-depth: 2
- name: FTP-Deploy-Action - name: FTP-Deploy-Action
uses: SamKirkland/FTP-Deploy-Action@3.0.0 uses: SamKirkland/FTP-Deploy-Action@3.1.2
with: with:
ftp-server: ftp://ftp.samkirkland.com/ ftp-server: ftp://ftp.samkirkland.com/
ftp-username: myFtpUserName ftp-username: myFtpUserName
ftp-password: ${{ secrets.FTP_PASSWORD }} ftp-password: ${{ secrets.FTP_PASSWORD }}
``` ```
#### Setup Steps ---
### Setup Steps
1. Select the repository you want to add the action to 1. Select the repository you want to add the action to
2. Select the `Actions` tab 2. Select the `Actions` tab
3. Select `Blank workflow file` or `Set up a workflow yourself`, if you don't see these options manually create a yaml file `Your_Project/.github/workflows/main.yml` 3. Select `Blank workflow file` or `Set up a workflow yourself`, if you don't see these options manually create a yaml file `Your_Project/.github/workflows/main.yml`
@@ -36,21 +41,28 @@ jobs:
5. Now you need to add a key to the `secrets` section in your project. To add a `secret` go to the `Settings` tab in your project then select `Secrets`. Add a new `Secret` for `ftp-password` 5. Now you need to add a key to the `secrets` section in your project. To add a `secret` go to the `Settings` tab in your project then select `Secrets`. Add a new `Secret` for `ftp-password`
6. Update your yaml file settings 6. Update your yaml file settings
__Note: Only tracked files will be published by default. If you want to publish files that don't exist in github (example: files generated during the action run) you must add those files/folders to `.git-ftp-include`__
Migrating from v2? See the [migration guide](v2-v3-migration.md)
---
### Settings ### Settings
**Migrating from v2? See the [migration guide](v2-v3-migration.md)**
Keys can be added directly to your .yml config file or referenced from your project `Secrets` storage. Keys can be added directly to your .yml config file or referenced from your project `Secrets` storage.
To add a `secret` go to the `Settings` tab in your project then select `Secrets`. To add a `secret` go to the `Settings` tab in your project then select `Secrets`.
I recommend you store your `ftp-password` as a secret. I recommend you store your `ftp-password` as a secret.
| Key Name | Required? | Example | Default | Description | | Key Name | Required? | Example | Default | Description |
|----------------|-----------|-----------------------------------------------|---------|----------------------------------------------------------| |----------------|-----------|--------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ftp-server` | Yes | ftp://ftp.samkirkland.com/destinationPath/ | | Deployment destination server & path. Formatted as `protocol://domain.com:port/destinationPath/` protocol can be `ftp`, `ftps`, or `sftp`. Port is optional, when not specified it will default to 21 when using ftp, 22 when using sftp, and 990 when using ftps | | `ftp-server` | Yes | ftp://ftp.samkirkland.com/destinationPath/ | | Deployment destination server & path. Formatted as `protocol://domain.com:port/destinationPath/` protocol can be `ftp`, `ftps`, or `sftp`. Port is optional, when not specified it will default to 21 when using ftp, 22 when using sftp, and 990 when using ftps |
| `ftp-username` | Yes | username@samkirkland.com | | FTP account username | | `ftp-username` | Yes | username@samkirkland.com | | FTP account username |
| `ftp-password` | Yes | CrazyUniquePassword&%123 | | FTP account password | | `ftp-password` | Yes | CrazyUniquePassword&%123 | | FTP account password |
| `local-dir` | No | deploy/ | ./ | Which local folder to deploy, path should be relative to the root and should include trailing slash. `./` is the root of the project | | `local-dir` | No | deploy/ | ./ | Which local folder to deploy, path should be relative to the root and should include trailing slash. `./` is the root of the project |
| `git-ftp-args` | No | See `git-ftp-args` section below | | Custom git-ftp arguments, this field is passed through directly into the git-ftp script | | `git-ftp-args` | No | See `git-ftp-args` section below | | Custom git-ftp arguments, this field is passed through directly into the git-ftp script |
| `known-hosts` | No | hostname ssh-rsa AAAAB3NzaC1y ... | | The desired contents of your .ssh/known_hosts file. See [known hosts setup](#known-hosts-setup) |
#### Advanced options using `git-ftp-args` #### Advanced options using `git-ftp-args`
Custom arguments, this field is passed through directly into the git-ftp script. See [git-ftp's manual](https://github.com/git-ftp/git-ftp/blob/master/man/git-ftp.1.md) for all options. Custom arguments, this field is passed through directly into the git-ftp script. See [git-ftp's manual](https://github.com/git-ftp/git-ftp/blob/master/man/git-ftp.1.md) for all options.
@@ -58,19 +70,18 @@ You can use as many arguments as you want, seperate them with a space
Below is an incomplete list of commonly used args: Below is an incomplete list of commonly used args:
| Argument | Description | | Argument | Description |
|------------------------|------------------------------------------------------------------------------------------------------| |-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--dry-run` | Does not upload or delete anything, but tries to get the .git-ftp.log file from remote host | | `--dry-run` | Does not upload or delete anything, but tries to get the .git-ftp.log file from remote host |
| `--silent` | Be silent | | `--silent` | Be silent |
| `--all` | Transfer all files, even seemingly the same as the target site (default is differences only) | | `--all` | Transfer all files, even seemingly the same as the target site (default is differences only). Note: Only files committed to github are uploaded, if you'd like to upload files generated during the action run see `.git-ftp-include` |
| `--lock` | Locks remote files from being modified while a deployment is running | | `--lock` | Locks remote files from being modified while a deployment is running |
| `--remote-root` | Specifies the remote root directory to deploy to. The remote path in the URL is ignored | | `--remote-root` | Specifies the remote root directory to deploy to. The remote path in the URL is ignored |
| `--key` | SSH private key file name for SFTP | | `--key` | SSH private key file name for SFTP |
| `--branch` | Push a specific branch. I recommend [creating a yaml action for each branch instead](https://github.com/SamKirkland/FTP-Deploy-Action/issues/37#issuecomment-579819486) | | `--branch` | Push a specific branch. I recommend [creating a yaml action for each branch instead](https://github.com/SamKirkland/FTP-Deploy-Action/issues/37#issuecomment-579819486) |
| `--pubkey` | SSH public key file name. Used with `--key` option | | `--pubkey` | SSH public key file name. Used with `--key` option |
| `--insecure` | Don't verify server's certificate | | `--insecure` | Don't verify server's certificate |
| `--cacert <file>` | Use as CA certificate store. Useful when a server has a self-signed certificate | | `--cacert <file>` | Use as CA certificate store. Useful when a server has a self-signed certificate |
### Ignore specific files when deploying ### Ignore specific files when deploying
Add patterns to `.git-ftp-ignore` and all matching file names will be ignored. The patterns are interpreted as shell glob patterns. Add patterns to `.git-ftp-ignore` and all matching file names will be ignored. The patterns are interpreted as shell glob patterns.
@@ -149,7 +160,10 @@ vendor/:composer.lock
``` ```
But keep in mind that this will upload all files in the vendor folder, even those that are on the server already. And it will not delete files from that directory if local files are deleted. But keep in mind that this will upload all files in the vendor folder, even those that are on the server already. And it will not delete files from that directory if local files are deleted.
### Common Examples ---
# Common Examples
Read more about the differences between these protocols [https://www.sftp.net/sftp-vs-ftps](https://www.sftp.net/sftp-vs-ftps) Read more about the differences between these protocols [https://www.sftp.net/sftp-vs-ftps](https://www.sftp.net/sftp-vs-ftps)
### FTP (File Transfer Protocol) ### FTP (File Transfer Protocol)
@@ -165,11 +179,11 @@ jobs:
name: FTP-Deploy-Action name: FTP-Deploy-Action
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v2.1.0
with: with:
fetch-depth: 2 fetch-depth: 2
- name: FTP-Deploy-Action - name: FTP-Deploy-Action
uses: SamKirkland/FTP-Deploy-Action@3.0.0 uses: SamKirkland/FTP-Deploy-Action@3.1.2
with: with:
ftp-server: ftp://ftp.samkirkland.com/ ftp-server: ftp://ftp.samkirkland.com/
ftp-username: myFtpUserName ftp-username: myFtpUserName
@@ -181,7 +195,7 @@ jobs:
Use the legacy FTP over a secure encrypted connection. Use the legacy FTP over a secure encrypted connection.
Notes about sftp: Notes about ftps:
- Most hosts don't offer FTPS, it's more common on windows/.net hosts and less common on linux hosting - Most hosts don't offer FTPS, it's more common on windows/.net hosts and less common on linux hosting
- Most hosts don't have a correct certificate setup for ftp domains, [even my host doesn't do it right](https://ftp.samkirkland.com/). This means you'll likely have to add `--insecure` to `git-ftp-args` - Most hosts don't have a correct certificate setup for ftp domains, [even my host doesn't do it right](https://ftp.samkirkland.com/). This means you'll likely have to add `--insecure` to `git-ftp-args`
@@ -193,12 +207,12 @@ jobs:
name: FTP-Deploy-Action name: FTP-Deploy-Action
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v2.1.0
with: with:
fetch-depth: 2 fetch-depth: 2
- name: FTP-Deploy-Action - name: FTP-Deploy-Action
uses: SamKirkland/FTP-Deploy-Action@3.0.0 uses: SamKirkland/FTP-Deploy-Action@3.1.2
with: with:
ftp-server: ftps://ftp.samkirkland.com:21/ ftp-server: ftps://ftp.samkirkland.com:21/
ftp-username: myFTPSUsername ftp-username: myFTPSUsername
@@ -217,6 +231,21 @@ Similar in name to FTP but this protocol is entirely new and requires SSH access
- You will need to create a **SSH** user to deploy over SFTP. Normally this is your cpanel or hosting providers username and password - You will need to create a **SSH** user to deploy over SFTP. Normally this is your cpanel or hosting providers username and password
- Most web hosts change the default port (21), check with your host for your port number - Most web hosts change the default port (21), check with your host for your port number
### Known Hosts Setup
**Windows**
In powershell run `ssh-keyscan -p <sshport> <hostname>` and copy the hash output
Example for samkirkland.com `ssh-keyscan -p 7822 samkirkland.com`
**Linux, or OSX (using homebrew)**
Install the OpenSSH packages and use `ssh-keyscan <hostname>` and copy the hash output
Add the `known-hosts` argument with your hosts hash
Example: `knownhosts: ssh-rsa AAAAB3Nza...H1Q5Spw==`
*Note: If you receive a `Connection refused` error, you must specify the ssh port to your host*
```yml ```yml
on: push on: push
name: Publish Website over SFTP name: Publish Website over SFTP
@@ -225,23 +254,24 @@ jobs:
name: FTP-Deploy-Action name: FTP-Deploy-Action
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v2.1.0
with: with:
fetch-depth: 2 fetch-depth: 2
- name: FTP-Deploy-Action - name: FTP-Deploy-Action
uses: SamKirkland/FTP-Deploy-Action@3.0.0 uses: SamKirkland/FTP-Deploy-Action@3.1.2
with: with:
ftp-server: sftp://ftp.samkirkland.com:7280/ ftp-server: sftp://ftp.samkirkland.com:7280/
ftp-username: mySFTPUsername ftp-username: mySFTPUsername
ftp-password: ${{ secrets.SFTP_PASSWORD }} ftp-password: ${{ secrets.SFTP_PASSWORD }}
git-ftp-args: --insecure # if your certificate is setup correctly this can be removed git-ftp-args: --insecure # if your certificate is setup correctly this can be removed (see known-hosts argument)
``` ```
### Build and Publish React/Angular/Vue Website ### Build and Publish React/Angular/Vue Website
Make sure you have an npm script named 'build'. This config should work for most node built websites. Make sure you have an npm script named 'build'. This config should work for most node built websites.
If you don't commit your `build` folder to github you MUST create a `.git-ftp-include` file with the content `!build/` so the folder is always uploaded
> #### If you don't commit your `build` folder to github you MUST create a `.git-ftp-include` file with the content `!build/` so the folder is always uploaded!
```yml ```yml
on: push on: push
name: Build and Publish Front End Framework Website name: Build and Publish Front End Framework Website
@@ -250,14 +280,14 @@ jobs:
name: FTP-Deploy-Action name: FTP-Deploy-Action
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v2.1.0
with: with:
fetch-depth: 2 fetch-depth: 2
- name: Use Node.js 12.x - name: Use Node.js 16
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: '12.x' node-version: '16'
- name: Build Project - name: Build Project
run: | run: |
@@ -268,12 +298,12 @@ jobs:
run: ls run: ls
- name: FTP-Deploy-Action - name: FTP-Deploy-Action
uses: SamKirkland/FTP-Deploy-Action@3.0.0 uses: SamKirkland/FTP-Deploy-Action@3.1.2
with: with:
ftp-server: ftp://ftp.samkirkland.com/ ftp-server: ftp://ftp.samkirkland.com/
ftp-username: myFTPUsername ftp-username: myFTPUsername
ftp-password: ${{ secrets.FTP_PASSWORD }} ftp-password: ${{ secrets.FTP_PASSWORD }}
local-dir: build local-dir: build # This folder is NOT going to upload by default unless you add it to .git-ftp-include
``` ```
### Log only dry run: Use this mode for testing ### Log only dry run: Use this mode for testing
@@ -286,12 +316,12 @@ jobs:
name: FTP-Deploy-Action name: FTP-Deploy-Action
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v2.1.0
with: with:
fetch-depth: 2 fetch-depth: 2
- name: FTP-Deploy-Action - name: FTP-Deploy-Action
uses: SamKirkland/FTP-Deploy-Action@3.0.0 uses: SamKirkland/FTP-Deploy-Action@3.1.2
with: with:
ftp-server: ftp://ftp.samkirkland.com/ ftp-server: ftp://ftp.samkirkland.com/
ftp-username: myFTPUsername ftp-username: myFTPUsername
@@ -299,36 +329,113 @@ jobs:
git-ftp-args: --dry-run git-ftp-args: --dry-run
``` ```
##### Want another example? Let me know by creating a github issue _Want another example? Let me know by creating a github issue_
---
## FAQ ## FAQ
1. `Can't access remote 'sftp://', exiting...` or `Can't access remote 'ftps://', exiting...` <details>
<summary>How to exclude .git files from the publish</summary>
See the [`.git-ftp-ignore`](#ignore-specific-files-when-deploying) example section
</details>
<details>
<summary>All files are being uploaded instead of just different files</summary>
By default only different files are uploaded.
Verify you have `with: fetch-depth: 2` in your `actions/checkout@v2.1.0` step. The last 2 checkins are required in order to determine differences
If you've had multiple git commits without deploying, all files will be uploaded to get back in sync
Verify you **don't** have the `--all` git-ftp-args flag set
</details>
<details>
<summary>How do I set a upload timeout?</summary>
github has a built-in `timeout-minutes` option, see customized example below
```yaml
on: push
name: Publish Website
jobs:
FTP-Deploy-Action:
name: FTP-Deploy-Action
runs-on: ubuntu-latest
timeout-minutes: 15 # time out after 15 minutes (default is 360 minutes)
steps:
....
```
</details>
---
## Common Errors
<details id="failed-to-upload">
<summary>Failed to upload files</summary>
* **Fix 1:** Verify your login credentials are correct, download a ftp client and test with the exact same host/username/password * **Fix 1:** Verify your login credentials are correct, download a ftp client and test with the exact same host/username/password
* **Fix 2:** Remember if you are using SFTP or FTPS you cannot use a normal FTP account username/password. You must use a elevated account. Each host has a different process to setup a FTPS or SFTP account. Please contact your host for help. * **Fix 2:** Remember if you are using SFTP or FTPS you cannot use a normal FTP account username/password. You must use a elevated account. Each host has a different process to setup a FTPS or SFTP account. Please contact your host for help.
* **Fix 3:** If you are using sftp or ftps you should add `git-ftp-args: --insecure`, most hosts setup certificates incorrectly :( * **Fix 3:** If you are using sftp or ftps you should add `git-ftp-args: --insecure`, most hosts setup certificates incorrectly :(
2. `rm: Access failed: 553 Prohibited file name: ./.ftpquota` </details>
* **What is happening?** The `.ftpquota` file is created by some FTP Servers and cannot be modified by the user
* **Fix:** Add `.ftpquota` to your `.git-ftp-ignore` file
3. How to exclude .git files from the publish
* **Fix:** See the `.git-ftp-ignore` example above
4. All files are being uploaded instead of just different files
* By default only different files are uploaded.
* Verify you have `with: fetch-depth: 2` in your `actions/checkout@master` step. The last 2 checkins are required in order to determine differences
* If you've had multiple git commits without deploying, all files will be uploaded to get back in sync
* Verify you **don't** have the `--all` git-ftp-args flag set
5. How do I set a upload timeout?
* github has a built-in `timeout-minutes` option. Place `timeout-minutes: X` before the `steps:` line. Timeout defaults to 360 minutes.
<details id="cant-access-remote-sftp">
<summary>Can't access remote 'sftp://', exiting...</summary>
### Debugging locally See ["Failed to upload files"](#failed-to-upload) section above
##### Instructions for debugging on windows </details>
- Install docker for windows
- Open powershell <details id="cant-access-remote-ftps">
<summary>Can't access remote 'ftps://', exiting...</summary>
See ["Failed to upload files"](#failed-to-upload) section above
</details>
<details id="files-arent_uploading">
<summary>My files aren't uploading</summary>
V3+ uses github to determine when files have changes and only publish differences. This means files that aren't committed to github will not upload by default.
To change this behavior please see [`.git-ftp-ignore`](#ignore-specific-files-when-deploying) documentation.
</details>
<details id="prohibited-file-name">
<summary>rm: Access failed: 553 Prohibited file name: ./.ftpquota</summary>
The `.ftpquota` file is created by some FTP Servers and cannot be modified by the user
Add `.ftpquota` to your [`.git-ftp-ignore`](#ignore-specific-files-when-deploying) file
</details>
<details id="ssl-peer-certificate">
<summary>Error: SSL peer certificate or SSH remote key was not OK</summary>
Whitelist your host via the `known-hosts` configuration option (see [known hosts setup](#known-hosts-setup) in SFTP) or add the `--insecure` argument
</details>
---
## Debugging locally
##### Instructions for debugging Windows
- [Install docker](https://docs.docker.com/get-docker/)
- Open powershell **as Administrator**
- Install [act-cli](https://github.com/nektos/act#installation) by running `choco install act-cli`
- Navigate to the repo folder - Navigate to the repo folder
- Run `docker build --tag action .` - Run `npm install` - this will install all dependencies to build this project
- Run `docker run action` - Run `npm build` - this will build the action javascript and watch/rebuild when files change
- Run `npm run build-docker` - this will build the docker container (only needs to be done once)
- Run `npm run run-docker` - this will spin up a local copy of the action defined in `/debug/local-debug-deployment.yaml`. Update package.json to set any secret values
##### Instructions for debugging on linux #### Instructions for debugging on Linux
- Please submit a PR for linux instructions :) - [Install docker](https://docs.docker.com/get-docker/) on a Debian-based distro you can run `sudo apt install docker docker.io`
- Open the terminal
- Install [act-cli](https://github.com/nektos/act#installation)
- Navigate to the repo folder
- Run `npm install` - this will install all dependencies to build this project
- Run `npm build` - this will build the action javascript and watch/rebuild when files change
- Run `npm run build-docker` - this will build the docker container (only needs to be done once)
- Run `npm run run-docker` - this will spin up a local copy of the action defined in `/debug/local-debug-deployment.yaml`. Update package.json to set any secret values
#### Pull Requests Welcome! #### Pull Requests Welcome!

View File

@@ -13,11 +13,11 @@ inputs:
required: true required: true
local-dir: local-dir:
description: 'The local folder to copy, defaults to root project folder' description: 'The local folder to copy, defaults to root project folder'
defaults: ./ default: ./
required: false required: false
git-ftp-args: git-ftp-args:
description: 'Passes through options into git-ftp' description: 'Passes through options into git-ftp'
defaults: default:
required: false required: false
runs: runs:
using: 'docker' using: 'docker'

View File

@@ -0,0 +1,18 @@
on: push
name: Local Debug Deployment
jobs:
Local-Debug-Deployment:
name: Local-Debug-Deployment
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.1.0
with:
fetch-depth: 2
- name: FTP-Deploy-Action
uses: ./
with:
ftp-server: ftp://ftp.samkirkland.com/
ftp-username: ${{ secrets.username }}
ftp-password: ${{ secrets.password }}
git-ftp-args: --dry-run

721
dist/index.js vendored
View File

@@ -43,7 +43,7 @@ module.exports =
/************************************************************************/ /************************************************************************/
/******/ ({ /******/ ({
/***/ 9: /***/ 1:
/***/ (function(__unusedmodule, exports, __webpack_require__) { /***/ (function(__unusedmodule, exports, __webpack_require__) {
"use strict"; "use strict";
@@ -58,9 +58,316 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}); });
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const os = __webpack_require__(87); const childProcess = __webpack_require__(129);
const events = __webpack_require__(614); const path = __webpack_require__(622);
const child = __webpack_require__(129); const util_1 = __webpack_require__(669);
const ioUtil = __webpack_require__(672);
const exec = util_1.promisify(childProcess.exec);
/**
* Copies a file or folder.
* Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js
*
* @param source source path
* @param dest destination path
* @param options optional. See CopyOptions.
*/
function cp(source, dest, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const { force, recursive } = readCopyOptions(options);
const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null;
// Dest is an existing file, but not forcing
if (destStat && destStat.isFile() && !force) {
return;
}
// If dest is an existing directory, should copy inside.
const newDest = destStat && destStat.isDirectory()
? path.join(dest, path.basename(source))
: dest;
if (!(yield ioUtil.exists(source))) {
throw new Error(`no such file or directory: ${source}`);
}
const sourceStat = yield ioUtil.stat(source);
if (sourceStat.isDirectory()) {
if (!recursive) {
throw new Error(`Failed to copy. ${source} is a directory, but tried to copy without recursive flag.`);
}
else {
yield cpDirRecursive(source, newDest, 0, force);
}
}
else {
if (path.relative(source, newDest) === '') {
// a file cannot be copied to itself
throw new Error(`'${newDest}' and '${source}' are the same file`);
}
yield copyFile(source, newDest, force);
}
});
}
exports.cp = cp;
/**
* Moves a path.
*
* @param source source path
* @param dest destination path
* @param options optional. See MoveOptions.
*/
function mv(source, dest, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
if (yield ioUtil.exists(dest)) {
let destExists = true;
if (yield ioUtil.isDirectory(dest)) {
// If dest is directory copy src into dest
dest = path.join(dest, path.basename(source));
destExists = yield ioUtil.exists(dest);
}
if (destExists) {
if (options.force == null || options.force) {
yield rmRF(dest);
}
else {
throw new Error('Destination already exists');
}
}
}
yield mkdirP(path.dirname(dest));
yield ioUtil.rename(source, dest);
});
}
exports.mv = mv;
/**
* Remove a path recursively with force
*
* @param inputPath path to remove
*/
function rmRF(inputPath) {
return __awaiter(this, void 0, void 0, function* () {
if (ioUtil.IS_WINDOWS) {
// Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another
// program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del.
try {
if (yield ioUtil.isDirectory(inputPath, true)) {
yield exec(`rd /s /q "${inputPath}"`);
}
else {
yield exec(`del /f /a "${inputPath}"`);
}
}
catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved
// other errors are valid
if (err.code !== 'ENOENT')
throw err;
}
// Shelling out fails to remove a symlink folder with missing source, this unlink catches that
try {
yield ioUtil.unlink(inputPath);
}
catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved
// other errors are valid
if (err.code !== 'ENOENT')
throw err;
}
}
else {
let isDir = false;
try {
isDir = yield ioUtil.isDirectory(inputPath);
}
catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved
// other errors are valid
if (err.code !== 'ENOENT')
throw err;
return;
}
if (isDir) {
yield exec(`rm -rf "${inputPath}"`);
}
else {
yield ioUtil.unlink(inputPath);
}
}
});
}
exports.rmRF = rmRF;
/**
* Make a directory. Creates the full path with folders in between
* Will throw if it fails
*
* @param fsPath path to create
* @returns Promise<void>
*/
function mkdirP(fsPath) {
return __awaiter(this, void 0, void 0, function* () {
yield ioUtil.mkdirP(fsPath);
});
}
exports.mkdirP = mkdirP;
/**
* Returns path of a tool had the tool actually been invoked. Resolves via paths.
* If you check and the tool does not exist, it will throw.
*
* @param tool name of the tool
* @param check whether to check if tool exists
* @returns Promise<string> path to tool
*/
function which(tool, check) {
return __awaiter(this, void 0, void 0, function* () {
if (!tool) {
throw new Error("parameter 'tool' is required");
}
// recursive when check=true
if (check) {
const result = yield which(tool, false);
if (!result) {
if (ioUtil.IS_WINDOWS) {
throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`);
}
else {
throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`);
}
}
}
try {
// build the list of extensions to try
const extensions = [];
if (ioUtil.IS_WINDOWS && process.env.PATHEXT) {
for (const extension of process.env.PATHEXT.split(path.delimiter)) {
if (extension) {
extensions.push(extension);
}
}
}
// if it's rooted, return it if exists. otherwise return empty.
if (ioUtil.isRooted(tool)) {
const filePath = yield ioUtil.tryGetExecutablePath(tool, extensions);
if (filePath) {
return filePath;
}
return '';
}
// if any path separators, return empty
if (tool.includes('/') || (ioUtil.IS_WINDOWS && tool.includes('\\'))) {
return '';
}
// build the list of directories
//
// Note, technically "where" checks the current directory on Windows. From a toolkit perspective,
// it feels like we should not do this. Checking the current directory seems like more of a use
// case of a shell, and the which() function exposed by the toolkit should strive for consistency
// across platforms.
const directories = [];
if (process.env.PATH) {
for (const p of process.env.PATH.split(path.delimiter)) {
if (p) {
directories.push(p);
}
}
}
// return the first match
for (const directory of directories) {
const filePath = yield ioUtil.tryGetExecutablePath(directory + path.sep + tool, extensions);
if (filePath) {
return filePath;
}
}
return '';
}
catch (err) {
throw new Error(`which failed with message ${err.message}`);
}
});
}
exports.which = which;
function readCopyOptions(options) {
const force = options.force == null ? true : options.force;
const recursive = Boolean(options.recursive);
return { force, recursive };
}
function cpDirRecursive(sourceDir, destDir, currentDepth, force) {
return __awaiter(this, void 0, void 0, function* () {
// Ensure there is not a run away recursive copy
if (currentDepth >= 255)
return;
currentDepth++;
yield mkdirP(destDir);
const files = yield ioUtil.readdir(sourceDir);
for (const fileName of files) {
const srcFile = `${sourceDir}/${fileName}`;
const destFile = `${destDir}/${fileName}`;
const srcFileStat = yield ioUtil.lstat(srcFile);
if (srcFileStat.isDirectory()) {
// Recurse
yield cpDirRecursive(srcFile, destFile, currentDepth, force);
}
else {
yield copyFile(srcFile, destFile, force);
}
}
// Change the mode for the newly created directory
yield ioUtil.chmod(destDir, (yield ioUtil.stat(sourceDir)).mode);
});
}
// Buffered file copy
function copyFile(srcFile, destFile, force) {
return __awaiter(this, void 0, void 0, function* () {
if ((yield ioUtil.lstat(srcFile)).isSymbolicLink()) {
// unlink/re-link it
try {
yield ioUtil.lstat(destFile);
yield ioUtil.unlink(destFile);
}
catch (e) {
// Try to override file permission
if (e.code === 'EPERM') {
yield ioUtil.chmod(destFile, '0666');
yield ioUtil.unlink(destFile);
}
// other errors = it doesn't exist, no work to do
}
// Copy over symlink
const symlinkFull = yield ioUtil.readlink(srcFile);
yield ioUtil.symlink(symlinkFull, destFile, ioUtil.IS_WINDOWS ? 'junction' : null);
}
else if (!(yield ioUtil.exists(destFile)) || force) {
yield ioUtil.copyFile(srcFile, destFile);
}
});
}
//# sourceMappingURL=io.js.map
/***/ }),
/***/ 9:
/***/ (function(__unusedmodule, exports, __webpack_require__) {
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const os = __importStar(__webpack_require__(87));
const events = __importStar(__webpack_require__(614));
const child = __importStar(__webpack_require__(129));
const path = __importStar(__webpack_require__(622));
const io = __importStar(__webpack_require__(1));
const ioUtil = __importStar(__webpack_require__(672));
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
const IS_WINDOWS = process.platform === 'win32'; const IS_WINDOWS = process.platform === 'win32';
/* /*
@@ -406,6 +713,16 @@ class ToolRunner extends events.EventEmitter {
*/ */
exec() { exec() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
// root the tool path if it is unrooted and contains relative pathing
if (!ioUtil.isRooted(this.toolPath) &&
(this.toolPath.includes('/') ||
(IS_WINDOWS && this.toolPath.includes('\\')))) {
// prefer options.cwd if it is specified, however options.cwd may also need to be rooted
this.toolPath = path.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath);
}
// if the tool is only a file name, then resolve it from the PATH
// otherwise verify it exists (add extension on Windows if necessary)
this.toolPath = yield io.which(this.toolPath, true);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._debug(`exec tool: ${this.toolPath}`); this._debug(`exec tool: ${this.toolPath}`);
this._debug('arguments:'); this._debug('arguments:');
@@ -494,6 +811,12 @@ class ToolRunner extends events.EventEmitter {
resolve(exitCode); resolve(exitCode);
} }
}); });
if (this.options.input) {
if (!cp.stdin) {
throw new Error('child process missing stdin');
}
cp.stdin.end(this.options.input);
}
}); });
}); });
} }
@@ -659,30 +982,58 @@ var __importStar = (this && this.__importStar) || function (mod) {
result["default"] = mod; result["default"] = mod;
return result; return result;
}; };
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470)); const core = __importStar(__webpack_require__(470));
const exec = __importStar(__webpack_require__(986)); const exec = __importStar(__webpack_require__(986));
const fs_1 = __importDefault(__webpack_require__(747));
const util_1 = __webpack_require__(669);
const writeFileAsync = util_1.promisify(fs_1.default.writeFile);
const errorDeploying = "⚠️ Error deploying";
function run() { function run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const userArguments = getUserArguments();
try { try {
const userArguments = getUserArguments();
yield configureHost(userArguments);
yield syncFiles(userArguments); yield syncFiles(userArguments);
console.log("✅ Deploy Complete"); console.log("✅ Deploy Complete");
} }
catch (error) { catch (error) {
console.error("⚠️ Error deploying"); console.error(errorDeploying);
core.setFailed(error.message); core.setFailed(error.message);
} }
}); });
} }
run(); run();
function configureHost(args) {
return __awaiter(this, void 0, void 0, function* () {
if (args.knownHosts === "") {
return;
}
try {
const sshFolder = `${process.env['HOME']}/.ssh`;
yield exec.exec(`mkdir -v -p ${sshFolder}`);
yield exec.exec(`chmod 700 ${sshFolder}`);
writeFileAsync(`${sshFolder}/known_hosts`, args.knownHosts);
yield exec.exec(`chmod 755 ${sshFolder}/known_hosts`);
console.log("✅ Configured known_hosts");
}
catch (error) {
console.error("⚠️ Error configuring known_hosts");
core.setFailed(error.message);
}
});
}
function getUserArguments() { function getUserArguments() {
return { return {
ftp_server: core.getInput("ftp-server", { required: true }), ftp_server: core.getInput("ftp-server", { required: true }),
ftp_username: core.getInput("ftp-username", { required: true }), ftp_username: core.getInput("ftp-username", { required: true }),
ftp_password: core.getInput("ftp-password", { required: true }), ftp_password: core.getInput("ftp-password", { required: true }),
local_dir: withDefault(core.getInput("local-dir"), "./"), local_dir: withDefault(core.getInput("local-dir"), "./"),
gitFtpArgs: withDefault(core.getInput("git-ftp-args"), "") gitFtpArgs: withDefault(core.getInput("git-ftp-args"), ""),
knownHosts: withDefault(core.getInput("known-hosts"), "")
}; };
} }
function withDefault(value, defaultValue) { function withDefault(value, defaultValue) {
@@ -698,18 +1049,32 @@ function syncFiles(args) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
yield core.group("Uploading files", () => __awaiter(this, void 0, void 0, function* () { yield core.group("Uploading files", () => __awaiter(this, void 0, void 0, function* () {
return yield exec.exec(`git ftp push --force --auto-init --verbose --syncroot ${args.local_dir} --user ${args.ftp_username} --passwd ${args.ftp_password} ${args.gitFtpArgs} ${args.ftp_server}`); return yield exec.exec("git ftp push", [
"--force",
"--auto-init",
"--verbose",
`--syncroot=${args.local_dir}`,
`--user=${args.ftp_username}`,
`--passwd=${args.ftp_password}`,
args.gitFtpArgs,
args.ftp_server
]);
})); }));
} }
catch (error) { catch (error) {
console.error("⚠️ Failed to upload files");
core.setFailed(error.message); core.setFailed(error.message);
throw error;
} }
}); });
} }
/***/ }),
/***/ 357:
/***/ (function(module) {
module.exports = require("assert");
/***/ }), /***/ }),
/***/ 431: /***/ 431:
@@ -717,17 +1082,24 @@ function syncFiles(args) {
"use strict"; "use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const os = __webpack_require__(87); const os = __importStar(__webpack_require__(87));
/** /**
* Commands * Commands
* *
* Command Format: * Command Format:
* ##[name key=value;key=value]message * ::name key=value,key=value::message
* *
* Examples: * Examples:
* ##[warning]This is the user warning message * ::warning::This is the message
* ##[set-secret name=mypassword]definitelyNotAPassword! * ::set-env name=MY_VAR::some value
*/ */
function issueCommand(command, properties, message) { function issueCommand(command, properties, message) {
const cmd = new Command(command, properties, message); const cmd = new Command(command, properties, message);
@@ -752,34 +1124,53 @@ class Command {
let cmdStr = CMD_STRING + this.command; let cmdStr = CMD_STRING + this.command;
if (this.properties && Object.keys(this.properties).length > 0) { if (this.properties && Object.keys(this.properties).length > 0) {
cmdStr += ' '; cmdStr += ' ';
let first = true;
for (const key in this.properties) { for (const key in this.properties) {
if (this.properties.hasOwnProperty(key)) { if (this.properties.hasOwnProperty(key)) {
const val = this.properties[key]; const val = this.properties[key];
if (val) { if (val) {
// safely append the val - avoid blowing up when attempting to if (first) {
// call .replace() if message is not a string for some reason first = false;
cmdStr += `${key}=${escape(`${val || ''}`)},`; }
else {
cmdStr += ',';
}
cmdStr += `${key}=${escapeProperty(val)}`;
} }
} }
} }
} }
cmdStr += CMD_STRING; cmdStr += `${CMD_STRING}${escapeData(this.message)}`;
// safely append the message - avoid blowing up when attempting to
// call .replace() if message is not a string for some reason
const message = `${this.message || ''}`;
cmdStr += escapeData(message);
return cmdStr; return cmdStr;
} }
} }
function escapeData(s) { /**
return s.replace(/\r/g, '%0D').replace(/\n/g, '%0A'); * Sanitizes an input into a string so it can be passed into issueCommand safely
* @param input input to sanitize into a string
*/
function toCommandValue(input) {
if (input === null || input === undefined) {
return '';
}
else if (typeof input === 'string' || input instanceof String) {
return input;
}
return JSON.stringify(input);
} }
function escape(s) { exports.toCommandValue = toCommandValue;
return s function escapeData(s) {
return toCommandValue(s)
.replace(/%/g, '%25')
.replace(/\r/g, '%0D')
.replace(/\n/g, '%0A');
}
function escapeProperty(s) {
return toCommandValue(s)
.replace(/%/g, '%25')
.replace(/\r/g, '%0D') .replace(/\r/g, '%0D')
.replace(/\n/g, '%0A') .replace(/\n/g, '%0A')
.replace(/]/g, '%5D') .replace(/:/g, '%3A')
.replace(/;/g, '%3B'); .replace(/,/g, '%2C');
} }
//# sourceMappingURL=command.js.map //# sourceMappingURL=command.js.map
@@ -799,10 +1190,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next()); step((generator = generator.apply(thisArg, _arguments || [])).next());
}); });
}; };
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const command_1 = __webpack_require__(431); const command_1 = __webpack_require__(431);
const os = __webpack_require__(87); const os = __importStar(__webpack_require__(87));
const path = __webpack_require__(622); const path = __importStar(__webpack_require__(622));
/** /**
* The code to exit an action * The code to exit an action
*/ */
@@ -823,11 +1221,13 @@ var ExitCode;
/** /**
* Sets env variable for this action and future actions in the job * Sets env variable for this action and future actions in the job
* @param name the name of the variable to set * @param name the name of the variable to set
* @param val the value of the variable * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function exportVariable(name, val) { function exportVariable(name, val) {
process.env[name] = val; const convertedVal = command_1.toCommandValue(val);
command_1.issueCommand('set-env', { name }, val); process.env[name] = convertedVal;
command_1.issueCommand('set-env', { name }, convertedVal);
} }
exports.exportVariable = exportVariable; exports.exportVariable = exportVariable;
/** /**
@@ -866,12 +1266,22 @@ exports.getInput = getInput;
* Sets the value of an output. * Sets the value of an output.
* *
* @param name name of the output to set * @param name name of the output to set
* @param value value to store * @param value value to store. Non-string values will be converted to a string via JSON.stringify
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setOutput(name, value) { function setOutput(name, value) {
command_1.issueCommand('set-output', { name }, value); command_1.issueCommand('set-output', { name }, value);
} }
exports.setOutput = setOutput; exports.setOutput = setOutput;
/**
* Enables or disables the echoing of commands into stdout for the rest of the step.
* Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set.
*
*/
function setCommandEcho(enabled) {
command_1.issue('echo', enabled ? 'on' : 'off');
}
exports.setCommandEcho = setCommandEcho;
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
// Results // Results
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@@ -888,6 +1298,13 @@ exports.setFailed = setFailed;
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
// Logging Commands // Logging Commands
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
/**
* Gets whether Actions Step Debug is on or not
*/
function isDebug() {
return process.env['RUNNER_DEBUG'] === '1';
}
exports.isDebug = isDebug;
/** /**
* Writes debug message to user log * Writes debug message to user log
* @param message debug message * @param message debug message
@@ -898,18 +1315,18 @@ function debug(message) {
exports.debug = debug; exports.debug = debug;
/** /**
* Adds an error issue * Adds an error issue
* @param message error issue message * @param message error issue message. Errors will be converted to string via toString()
*/ */
function error(message) { function error(message) {
command_1.issue('error', message); command_1.issue('error', message instanceof Error ? message.toString() : message);
} }
exports.error = error; exports.error = error;
/** /**
* Adds an warning issue * Adds an warning issue
* @param message warning issue message * @param message warning issue message. Errors will be converted to string via toString()
*/ */
function warning(message) { function warning(message) {
command_1.issue('warning', message); command_1.issue('warning', message instanceof Error ? message.toString() : message);
} }
exports.warning = warning; exports.warning = warning;
/** /**
@@ -967,8 +1384,9 @@ exports.group = group;
* Saves state for current action, the state can only be retrieved by this action's post job execution. * Saves state for current action, the state can only be retrieved by this action's post job execution.
* *
* @param name name of the state to store * @param name name of the state to store
* @param value value to store * @param value value to store. Non-string values will be converted to a string via JSON.stringify
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function saveState(name, value) { function saveState(name, value) {
command_1.issueCommand('save-state', { name }, value); command_1.issueCommand('save-state', { name }, value);
} }
@@ -1001,6 +1419,222 @@ module.exports = require("path");
/***/ }), /***/ }),
/***/ 669:
/***/ (function(module) {
module.exports = require("util");
/***/ }),
/***/ 672:
/***/ (function(__unusedmodule, exports, __webpack_require__) {
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
const assert_1 = __webpack_require__(357);
const fs = __webpack_require__(747);
const path = __webpack_require__(622);
_a = fs.promises, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink;
exports.IS_WINDOWS = process.platform === 'win32';
function exists(fsPath) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield exports.stat(fsPath);
}
catch (err) {
if (err.code === 'ENOENT') {
return false;
}
throw err;
}
return true;
});
}
exports.exists = exists;
function isDirectory(fsPath, useStat = false) {
return __awaiter(this, void 0, void 0, function* () {
const stats = useStat ? yield exports.stat(fsPath) : yield exports.lstat(fsPath);
return stats.isDirectory();
});
}
exports.isDirectory = isDirectory;
/**
* On OSX/Linux, true if path starts with '/'. On Windows, true for paths like:
* \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases).
*/
function isRooted(p) {
p = normalizeSeparators(p);
if (!p) {
throw new Error('isRooted() parameter "p" cannot be empty');
}
if (exports.IS_WINDOWS) {
return (p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello
); // e.g. C: or C:\hello
}
return p.startsWith('/');
}
exports.isRooted = isRooted;
/**
* Recursively create a directory at `fsPath`.
*
* This implementation is optimistic, meaning it attempts to create the full
* path first, and backs up the path stack from there.
*
* @param fsPath The path to create
* @param maxDepth The maximum recursion depth
* @param depth The current recursion depth
*/
function mkdirP(fsPath, maxDepth = 1000, depth = 1) {
return __awaiter(this, void 0, void 0, function* () {
assert_1.ok(fsPath, 'a path argument must be provided');
fsPath = path.resolve(fsPath);
if (depth >= maxDepth)
return exports.mkdir(fsPath);
try {
yield exports.mkdir(fsPath);
return;
}
catch (err) {
switch (err.code) {
case 'ENOENT': {
yield mkdirP(path.dirname(fsPath), maxDepth, depth + 1);
yield exports.mkdir(fsPath);
return;
}
default: {
let stats;
try {
stats = yield exports.stat(fsPath);
}
catch (err2) {
throw err;
}
if (!stats.isDirectory())
throw err;
}
}
}
});
}
exports.mkdirP = mkdirP;
/**
* Best effort attempt to determine whether a file exists and is executable.
* @param filePath file path to check
* @param extensions additional file extensions to try
* @return if file exists and is executable, returns the file path. otherwise empty string.
*/
function tryGetExecutablePath(filePath, extensions) {
return __awaiter(this, void 0, void 0, function* () {
let stats = undefined;
try {
// test file exists
stats = yield exports.stat(filePath);
}
catch (err) {
if (err.code !== 'ENOENT') {
// eslint-disable-next-line no-console
console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`);
}
}
if (stats && stats.isFile()) {
if (exports.IS_WINDOWS) {
// on Windows, test for valid extension
const upperExt = path.extname(filePath).toUpperCase();
if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) {
return filePath;
}
}
else {
if (isUnixExecutable(stats)) {
return filePath;
}
}
}
// try each extension
const originalFilePath = filePath;
for (const extension of extensions) {
filePath = originalFilePath + extension;
stats = undefined;
try {
stats = yield exports.stat(filePath);
}
catch (err) {
if (err.code !== 'ENOENT') {
// eslint-disable-next-line no-console
console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`);
}
}
if (stats && stats.isFile()) {
if (exports.IS_WINDOWS) {
// preserve the case of the actual file (since an extension was appended)
try {
const directory = path.dirname(filePath);
const upperName = path.basename(filePath).toUpperCase();
for (const actualName of yield exports.readdir(directory)) {
if (upperName === actualName.toUpperCase()) {
filePath = path.join(directory, actualName);
break;
}
}
}
catch (err) {
// eslint-disable-next-line no-console
console.log(`Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}`);
}
return filePath;
}
else {
if (isUnixExecutable(stats)) {
return filePath;
}
}
}
}
return '';
});
}
exports.tryGetExecutablePath = tryGetExecutablePath;
function normalizeSeparators(p) {
p = p || '';
if (exports.IS_WINDOWS) {
// convert slashes on Windows
p = p.replace(/\//g, '\\');
// remove redundant slashes
return p.replace(/\\\\+/g, '\\');
}
// remove redundant slashes
return p.replace(/\/\/+/g, '/');
}
// on Mac/Linux, test the execute bit
// R W X R W X R W X
// 256 128 64 32 16 8 4 2 1
function isUnixExecutable(stats) {
return ((stats.mode & 1) > 0 ||
((stats.mode & 8) > 0 && stats.gid === process.getgid()) ||
((stats.mode & 64) > 0 && stats.uid === process.getuid()));
}
//# sourceMappingURL=io-util.js.map
/***/ }),
/***/ 747:
/***/ (function(module) {
module.exports = require("fs");
/***/ }),
/***/ 986: /***/ 986:
/***/ (function(__unusedmodule, exports, __webpack_require__) { /***/ (function(__unusedmodule, exports, __webpack_require__) {
@@ -1015,8 +1649,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next()); step((generator = generator.apply(thisArg, _arguments || [])).next());
}); });
}; };
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const tr = __webpack_require__(9); const tr = __importStar(__webpack_require__(9));
/** /**
* Exec a command. * Exec a command.
* Output will be streamed to the live console. * Output will be streamed to the live console.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

3753
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,16 @@
{ {
"name": "typescript-action", "name": "typescript-action",
"version": "1.0.0", "version": "3.1.2",
"private": true, "private": true,
"description": "TypeScript template action", "description": "TypeScript template action",
"main": "dist/main.js", "main": "dist/main.js",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=16.0.0"
}, },
"scripts": { "scripts": {
"build:dev": "tsc", "build": "ncc build src/main.ts -o dist --watch",
"build:production": "ncc build src/main.ts -o dist", "build-docker": "docker build --tag action .",
"build:docker": "docker build --tag action .", "run-docker": "act --workflows ./debug/ --secret username=UserNameHere --secret password=PasswordHere"
"run:docker": "docker run action --build-arg FTP_SERVER=example.com FTP_USERNAME=test@example.com FTP_PASSWORD=passwordExample"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -25,16 +24,16 @@
"author": "SamKirkland", "author": "SamKirkland",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "1.2.0", "@actions/core": "1.2.4",
"@actions/exec": "1.0.1" "@actions/exec": "1.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "24.0.23", "@types/jest": "25.2.1",
"@types/node": "12.12.8", "@types/node": "12.12.8",
"@zeit/ncc": "0.20.5", "@zeit/ncc": "0.22.1",
"jest": "24.9.0", "jest": "25.5.3",
"jest-circus": "24.9.0", "jest-circus": "25.5.3",
"ts-jest": "24.1.0", "ts-jest": "25.4.0",
"typescript": "3.7.2" "typescript": "3.8.3"
} }
} }

View File

@@ -1,23 +1,49 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as exec from '@actions/exec'; import * as exec from '@actions/exec';
import fs from 'fs';
import { promisify } from 'util';
import { IActionArguments } from './types'; import { IActionArguments } from './types';
async function run() { const writeFileAsync = promisify(fs.writeFile);
const userArguments = getUserArguments(); const errorDeploying = "⚠️ Error deploying";
async function run() {
try { try {
const userArguments = getUserArguments();
await configureHost(userArguments);
await syncFiles(userArguments); await syncFiles(userArguments);
console.log("✅ Deploy Complete"); console.log("✅ Deploy Complete");
} }
catch (error) { catch (error) {
console.error("⚠️ Error deploying"); console.error(errorDeploying);
core.setFailed(error.message); core.setFailed(error.message);
} }
} }
run(); run();
async function configureHost(args: IActionArguments): Promise<void> {
if (args.knownHosts === "") {
return;
}
try {
const sshFolder = `${process.env['HOME']}/.ssh`;
await exec.exec(`mkdir -v -p ${sshFolder}`);
await exec.exec(`chmod 700 ${sshFolder}`);
writeFileAsync(`${sshFolder}/known_hosts`, args.knownHosts);
await exec.exec(`chmod 755 ${sshFolder}/known_hosts`);
console.log("✅ Configured known_hosts");
}
catch (error) {
console.error("⚠️ Error configuring known_hosts");
core.setFailed(error.message);
}
}
function getUserArguments(): IActionArguments { function getUserArguments(): IActionArguments {
return { return {
@@ -25,7 +51,8 @@ function getUserArguments(): IActionArguments {
ftp_username: core.getInput("ftp-username", { required: true }), ftp_username: core.getInput("ftp-username", { required: true }),
ftp_password: core.getInput("ftp-password", { required: true }), ftp_password: core.getInput("ftp-password", { required: true }),
local_dir: withDefault(core.getInput("local-dir"), "./"), local_dir: withDefault(core.getInput("local-dir"), "./"),
gitFtpArgs: withDefault(core.getInput("git-ftp-args"), "") gitFtpArgs: withDefault(core.getInput("git-ftp-args"), ""),
knownHosts: withDefault(core.getInput("known-hosts"), "")
}; };
} }
@@ -43,12 +70,22 @@ function withDefault(value: string, defaultValue: string) {
async function syncFiles(args: IActionArguments) { async function syncFiles(args: IActionArguments) {
try { try {
await core.group("Uploading files", async () => { await core.group("Uploading files", async () => {
return await exec.exec(`git ftp push --force --auto-init --verbose --syncroot ${args.local_dir} --user ${args.ftp_username} --passwd ${args.ftp_password} ${args.gitFtpArgs} ${args.ftp_server}`); return await exec.exec(
"git ftp push",
[
"--force",
"--auto-init",
"--verbose",
`--syncroot=${args.local_dir}`,
`--user=${args.ftp_username}`,
`--passwd=${args.ftp_password}`,
args.gitFtpArgs!,
args.ftp_server!
]
);
}); });
} }
catch (error) { catch (error) {
console.error("⚠️ Failed to upload files");
core.setFailed(error.message); core.setFailed(error.message);
throw error;
} }
} }

View File

@@ -8,6 +8,9 @@ export interface IActionArguments {
/** @default "" */ /** @default "" */
gitFtpArgs: string | undefined; gitFtpArgs: string | undefined;
/** @default "" */
knownHosts: string | undefined;
} }
/** /**
@@ -22,7 +25,7 @@ export enum gitFTPExitCode {
ErrorWhileDownloading = 5, ErrorWhileDownloading = 5,
UnknownProtocol = 6, UnknownProtocol = 6,
RemoteLocked = 7, RemoteLocked = 7,
NotAGitProject = 8, GitRelatedError = 8,
PreFTPPushHookFailed = 9, PreFTPPushHookFailed = 9,
LocalFileOperationFailed = 10 LocalFileOperationFailed = 10
} }

View File

@@ -1,6 +1,6 @@
# Migrating from v2 to v3 # Migrating from v2 to v3
`uses: actions/checkout@master` must now have the option `fetch-depth: 2` `uses: actions/checkout@v2.1.0` must now have the option `fetch-depth: 2`
Without the `fetch-depth` option diffs cannot be calculated and all files will be uploaded. Without the `fetch-depth` option diffs cannot be calculated and all files will be uploaded.