mirror of
https://github.com/redhat-actions/push-to-registry.git
synced 2025-02-22 18:21:20 +01:00
Add feature to push manifest (#55)
* Add feature to push manifest Signed-off-by: divyansh42 <diagrawa@redhat.com>
This commit is contained in:
parent
3220bde582
commit
56f05cb637
7 changed files with 209 additions and 78 deletions
75
.github/workflows/manifest-build-push.yaml
vendored
Normal file
75
.github/workflows/manifest-build-push.yaml
vendored
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# This workflow will perform a test whenever there
|
||||||
|
# is some change in code done to ensure that the changes
|
||||||
|
# are not buggy and we are getting the desired output.
|
||||||
|
name: Build and Push Manifest
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *' # every day at midnight
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: ptr-manifest
|
||||||
|
IMAGE_TAGS: v1 ${{ github.sha }}
|
||||||
|
IMAGE_REGISTRY: quay.io
|
||||||
|
IMAGE_NAMESPACE: redhat-github-actions
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push-quay:
|
||||||
|
name: Build and push manifest
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
install_latest: [ true, false ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Checkout push-to-registry action github repository
|
||||||
|
- name: Checkout Push to Registry action
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install latest podman
|
||||||
|
if: matrix.install_latest
|
||||||
|
run: |
|
||||||
|
bash .github/install_latest_podman.sh
|
||||||
|
|
||||||
|
- name: Install qemu dependency
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y qemu-user-static
|
||||||
|
|
||||||
|
- name: Create Containerfile
|
||||||
|
run: |
|
||||||
|
cat > Containerfile<<EOF
|
||||||
|
|
||||||
|
FROM docker.io/alpine:3.14
|
||||||
|
|
||||||
|
RUN echo "hello world"
|
||||||
|
|
||||||
|
ENTRYPOINT [ "sh", "-c", "echo -n 'Machine: ' && uname -m && echo -n 'Bits: ' && getconf LONG_BIT && echo 'goodbye world'" ]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Build Image
|
||||||
|
id: build_image
|
||||||
|
uses: redhat-actions/buildah-build@main
|
||||||
|
with:
|
||||||
|
image: ${{ env.IMAGE_NAME }}
|
||||||
|
tags: ${{ env.IMAGE_TAGS }}
|
||||||
|
archs: amd64, arm64
|
||||||
|
containerfiles: |
|
||||||
|
./Containerfile
|
||||||
|
|
||||||
|
# Push the image manifest to Quay.io (Image Registry)
|
||||||
|
- name: Push To Quay
|
||||||
|
uses: ./
|
||||||
|
id: push
|
||||||
|
with:
|
||||||
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
|
tags: ${{ steps.build_image.outputs.tags }}
|
||||||
|
registry: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAMESPACE }}
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Echo outputs
|
||||||
|
run: |
|
||||||
|
echo "${{ toJSON(steps.push.outputs) }}"
|
12
README.md
12
README.md
|
@ -12,7 +12,7 @@
|
||||||
[](./LICENSE)
|
[](./LICENSE)
|
||||||
[](./dist)
|
[](./dist)
|
||||||
|
|
||||||
Push-to-registry is a GitHub Action for pushing a container image to an image registry, such as Dockerhub, quay.io, the GitHub Container Registry, or an OpenShift integrated registry.
|
Push-to-registry is a GitHub Action for pushing a container image or an [image manifest](https://github.com/containers/buildah/blob/main/docs/buildah-manifest.1.md) to an image registry, such as Dockerhub, quay.io, the GitHub Container Registry, or an OpenShift integrated registry.
|
||||||
|
|
||||||
This action only runs on Linux, as it uses [podman](https://github.com/containers/Podman) to perform the push. [GitHub's Ubuntu action runners](https://github.com/actions/virtual-environments#available-environments) come with Podman preinstalled. If you are not using those runners, you must first [install Podman](https://podman.io/getting-started/installation).
|
This action only runs on Linux, as it uses [podman](https://github.com/containers/Podman) to perform the push. [GitHub's Ubuntu action runners](https://github.com/actions/virtual-environments#available-environments) come with Podman preinstalled. If you are not using those runners, you must first [install Podman](https://podman.io/getting-started/installation).
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ Refer to the [`podman push`](http://docs.podman.io/en/latest/markdown/podman-man
|
||||||
|
|
||||||
| Input Name | Description | Default |
|
| Input Name | Description | Default |
|
||||||
| ---------- | ----------- | ------- |
|
| ---------- | ----------- | ------- |
|
||||||
| image | Name of the image you want to push. Eg. `username/imagename` or `imagename`. Refer to [Image and Tag Inputs](https://github.com/redhat-actions/push-to-registry#image-tag-inputs). | **Required** - unless all tags include registry and image name
|
| image | Name of the image or manifest you want to push. Eg. `username/imagename` or `imagename`. Refer to [Image and Tag Inputs](https://github.com/redhat-actions/push-to-registry#image-tag-inputs). | **Required** - unless all tags include registry and image name
|
||||||
| tags | The tag or tags of the image to push. For multiple tags, separate by whitespace. Refer to [Image and Tag Inputs](https://github.com/redhat-actions/push-to-registry#image-tag-inputs). | `latest`
|
| tags | The tag or tags of the image or manifest to push. For multiple tags, separate by whitespace. Refer to [Image and Tag Inputs](https://github.com/redhat-actions/push-to-registry#image-tag-inputs). | `latest`
|
||||||
| registry | Hostname and optional namespace to push the image to. Eg. `quay.io` or `quay.io/username`. Refer to [Image and Tag Inputs](https://github.com/redhat-actions/push-to-registry#image-tag-inputs). | **Required** - unless all tags include registry and image name
|
| registry | Hostname and optional namespace to push the image to. Eg. `quay.io` or `quay.io/username`. Refer to [Image and Tag Inputs](https://github.com/redhat-actions/push-to-registry#image-tag-inputs). | **Required** - unless all tags include registry and image name
|
||||||
| username | Username with which to authenticate to the registry. Required unless already logged in to the registry. | None
|
| username | Username with which to authenticate to the registry. Required unless already logged in to the registry. | None
|
||||||
| password | Password, encrypted password, or access token to use to log in to the registry. Required unless already logged in to the registry. | None
|
| password | Password, encrypted password, or access token to use to log in to the registry. Required unless already logged in to the registry. | None
|
||||||
|
@ -83,6 +83,12 @@ For example:
|
||||||
|
|
||||||
`registry-path`: The first element of `registry-paths`, as a string.
|
`registry-path`: The first element of `registry-paths`, as a string.
|
||||||
|
|
||||||
|
## Pushing Manifest
|
||||||
|
|
||||||
|
If multiple tags are provided, either all tags must point to manifests, or none of them. i.e., you cannot push both manifests are regular images in one `push-to-registry` step.
|
||||||
|
|
||||||
|
Refer to [Manifest Build and Push example](./.github/workflows/manifest-build-push.yaml) for a sophisticated example of building and pushing a manifest.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
The example below shows how the `push-to-registry` action can be used to push an image created by the [**buildah-build**](https://github.com/redhat-actions/buildah-build) action.
|
The example below shows how the `push-to-registry` action can be used to push an image created by the [**buildah-build**](https://github.com/redhat-actions/buildah-build) action.
|
||||||
|
|
|
@ -6,10 +6,12 @@ branding:
|
||||||
color: red
|
color: red
|
||||||
inputs:
|
inputs:
|
||||||
image:
|
image:
|
||||||
description: 'Name of the image to push (e.g. username/imagename or imagename)'
|
description: 'Name of the image/manifest to push (e.g. username/imagename or imagename)'
|
||||||
required: false
|
required: false
|
||||||
tags:
|
tags:
|
||||||
description: 'The tag or tags of the image to push. For multiple tags, seperate by whitespace. For example, "latest v1"'
|
description: |
|
||||||
|
'The tag or tags of the image/manifest to push.
|
||||||
|
For multiple tags, seperate by whitespace. For example, "latest v1"'
|
||||||
required: false
|
required: false
|
||||||
default: 'latest'
|
default: 'latest'
|
||||||
registry:
|
registry:
|
||||||
|
@ -39,7 +41,7 @@ inputs:
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
digest:
|
digest:
|
||||||
description: 'The pushed image digest, as written to the "digestfile"'
|
description: 'The pushed image/manifest digest, as written to the "digestfile"'
|
||||||
registry-path:
|
registry-path:
|
||||||
description: 'The first element of registry-paths.'
|
description: 'The first element of registry-paths.'
|
||||||
registry-paths:
|
registry-paths:
|
||||||
|
|
2
dist/index.js
vendored
2
dist/index.js
vendored
File diff suppressed because one or more lines are too long
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
|
@ -16,7 +16,7 @@ export enum Inputs {
|
||||||
*/
|
*/
|
||||||
EXTRA_ARGS = "extra-args",
|
EXTRA_ARGS = "extra-args",
|
||||||
/**
|
/**
|
||||||
* Name of the image to push (e.g. username/imagename or imagename)
|
* Name of the image/manifest to push (e.g. username/imagename or imagename)
|
||||||
* Required: false
|
* Required: false
|
||||||
* Default: None.
|
* Default: None.
|
||||||
*/
|
*/
|
||||||
|
@ -34,7 +34,8 @@ export enum Inputs {
|
||||||
*/
|
*/
|
||||||
REGISTRY = "registry",
|
REGISTRY = "registry",
|
||||||
/**
|
/**
|
||||||
* The tag or tags of the image to push. For multiple tags, seperate by whitespace. For example, "latest v1"
|
* 'The tag or tags of the image/manifest to push.
|
||||||
|
* For multiple tags, seperate by whitespace. For example, "latest v1"'
|
||||||
* Required: false
|
* Required: false
|
||||||
* Default: "latest"
|
* Default: "latest"
|
||||||
*/
|
*/
|
||||||
|
@ -55,7 +56,7 @@ export enum Inputs {
|
||||||
|
|
||||||
export enum Outputs {
|
export enum Outputs {
|
||||||
/**
|
/**
|
||||||
* The pushed image digest, as written to the "digestfile"
|
* The pushed image/manifest digest, as written to the "digestfile"
|
||||||
* Required: false
|
* Required: false
|
||||||
* Default: None.
|
* Default: None.
|
||||||
*/
|
*/
|
||||||
|
|
181
src/index.ts
181
src/index.ts
|
@ -107,87 +107,92 @@ async function run(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const registryPathList: string[] = [];
|
const registryPathList: string[] = [];
|
||||||
|
// here
|
||||||
|
// check if provided image is manifest or not
|
||||||
|
const isManifest = await checkIfManifestsExists();
|
||||||
|
|
||||||
// check if image with all the required tags exist in Podman image storage
|
if (!isManifest) {
|
||||||
const podmanImageStorageCheckResult: ImageStorageCheckResult = await checkImageInPodman();
|
// check if image with all the required tags exist in Podman image storage
|
||||||
|
const podmanImageStorageCheckResult: ImageStorageCheckResult = await checkImageInPodman();
|
||||||
|
|
||||||
const podmanFoundTags: string[] = podmanImageStorageCheckResult.foundTags;
|
const podmanFoundTags: string[] = podmanImageStorageCheckResult.foundTags;
|
||||||
const podmanMissingTags: string[] = podmanImageStorageCheckResult.missingTags;
|
const podmanMissingTags: string[] = podmanImageStorageCheckResult.missingTags;
|
||||||
|
|
||||||
if (podmanFoundTags.length > 0) {
|
if (podmanFoundTags.length > 0) {
|
||||||
core.info(`Tag${podmanFoundTags.length !== 1 ? "s" : ""} "${podmanFoundTags.join(", ")}" `
|
core.info(`Tag${podmanFoundTags.length !== 1 ? "s" : ""} "${podmanFoundTags.join(", ")}" `
|
||||||
+ `found in Podman image storage`);
|
+ `found in Podman image storage`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log warning if few tags are not found
|
// Log warning if few tags are not found
|
||||||
if (podmanMissingTags.length > 0 && podmanFoundTags.length > 0) {
|
if (podmanMissingTags.length > 0 && podmanFoundTags.length > 0) {
|
||||||
core.warning(`Tag${podmanMissingTags.length !== 1 ? "s" : ""} "${podmanMissingTags.join(", ")}" `
|
core.warning(`Tag${podmanMissingTags.length !== 1 ? "s" : ""} "${podmanMissingTags.join(", ")}" `
|
||||||
+ `not found in Podman image storage`);
|
+ `not found in Podman image storage`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if image with all the required tags exist in Docker image storage
|
// check if image with all the required tags exist in Docker image storage
|
||||||
// and if exist pull the image with all the tags to Podman
|
// and if exist pull the image with all the tags to Podman
|
||||||
const dockerImageStorageCheckResult: ImageStorageCheckResult = await pullImageFromDocker();
|
const dockerImageStorageCheckResult: ImageStorageCheckResult = await pullImageFromDocker();
|
||||||
|
|
||||||
const dockerFoundTags: string[] = dockerImageStorageCheckResult.foundTags;
|
const dockerFoundTags: string[] = dockerImageStorageCheckResult.foundTags;
|
||||||
const dockerMissingTags: string[] = dockerImageStorageCheckResult.missingTags;
|
const dockerMissingTags: string[] = dockerImageStorageCheckResult.missingTags;
|
||||||
|
|
||||||
if (dockerFoundTags.length > 0) {
|
if (dockerFoundTags.length > 0) {
|
||||||
core.info(`Tag${dockerFoundTags.length !== 1 ? "s" : ""} "${dockerFoundTags.join(", ")}" `
|
core.info(`Tag${dockerFoundTags.length !== 1 ? "s" : ""} "${dockerFoundTags.join(", ")}" `
|
||||||
+ `found in Docker image storage`);
|
+ `found in Docker image storage`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log warning if few tags are not found
|
// Log warning if few tags are not found
|
||||||
if (dockerMissingTags.length > 0 && dockerFoundTags.length > 0) {
|
if (dockerMissingTags.length > 0 && dockerFoundTags.length > 0) {
|
||||||
core.warning(`Tag${dockerMissingTags.length !== 1 ? "s" : ""} "${dockerMissingTags.join(", ")}" `
|
core.warning(`Tag${dockerMissingTags.length !== 1 ? "s" : ""} "${dockerMissingTags.join(", ")}" `
|
||||||
+ `not found in Docker image storage`);
|
+ `not found in Docker image storage`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// failing if image with any of the tag is not found in Docker as well as Podman
|
// failing if image with any of the tag is not found in Docker as well as Podman
|
||||||
if (podmanMissingTags.length > 0 && dockerMissingTags.length > 0) {
|
if (podmanMissingTags.length > 0 && dockerMissingTags.length > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`❌ All tags were not found in either Podman image storage, or Docker image storage. `
|
`❌ All tags were not found in either Podman image storage, or Docker image storage. `
|
||||||
+ `Tag${podmanMissingTags.length !== 1 ? "s" : ""} "${podmanMissingTags.join(", ")}" `
|
+ `Tag${podmanMissingTags.length !== 1 ? "s" : ""} "${podmanMissingTags.join(", ")}" `
|
||||||
+ `not found in Podman image storage, and tag${dockerMissingTags.length !== 1 ? "s" : ""} `
|
+ `not found in Podman image storage, and tag${dockerMissingTags.length !== 1 ? "s" : ""} `
|
||||||
+ `"${dockerMissingTags.join(", ")}" not found in Docker image storage.`
|
+ `"${dockerMissingTags.join(", ")}" not found in Docker image storage.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTagsinPodman: boolean = podmanFoundTags.length === tagsList.length;
|
const allTagsinPodman: boolean = podmanFoundTags.length === tagsList.length;
|
||||||
const allTagsinDocker: boolean = dockerFoundTags.length === tagsList.length;
|
const allTagsinDocker: boolean = dockerFoundTags.length === tagsList.length;
|
||||||
|
|
||||||
if (allTagsinPodman && allTagsinDocker) {
|
if (allTagsinPodman && allTagsinDocker) {
|
||||||
const isPodmanImageLatest = await isPodmanLocalImageLatest();
|
const isPodmanImageLatest = await isPodmanLocalImageLatest();
|
||||||
if (!isPodmanImageLatest) {
|
if (!isPodmanImageLatest) {
|
||||||
core.warning(
|
core.warning(
|
||||||
`The version of "${sourceImages[0]}" in the Docker image storage is more recent `
|
`The version of "${sourceImages[0]}" in the Docker image storage is more recent `
|
||||||
+ `than the version in the Podman image storage. The image(s) from the Docker image storage `
|
+ `than the version in the Podman image storage. The image(s) from the Docker image storage `
|
||||||
+ `will be pushed.`
|
+ `will be pushed.`
|
||||||
|
);
|
||||||
|
isImageFromDocker = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.warning(
|
||||||
|
`The version of "${sourceImages[0]}" in the Podman image storage is more recent `
|
||||||
|
+ `than the version in the Docker image storage. The image(s) from the Podman image `
|
||||||
|
+ `storage will be pushed.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (allTagsinDocker) {
|
||||||
|
core.info(
|
||||||
|
`Tag "${sourceImages[0]}" was found in the Docker image storage, but not in the Podman `
|
||||||
|
+ `image storage. The image(s) will be pulled into Podman image storage, pushed, and then `
|
||||||
|
+ `removed from the Podman image storage.`
|
||||||
);
|
);
|
||||||
isImageFromDocker = true;
|
isImageFromDocker = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
core.warning(
|
core.info(
|
||||||
`The version of "${sourceImages[0]}" in the Podman image storage is more recent `
|
`Tag "${sourceImages[0]}" was found in the Podman image storage, but not in the Docker `
|
||||||
+ `than the version in the Docker image storage. The image(s) from the Podman image `
|
+ `image storage. The image(s) will be pushed from Podman image storage.`
|
||||||
+ `storage will be pushed.`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (allTagsinDocker) {
|
|
||||||
core.info(
|
|
||||||
`Tag "${sourceImages[0]}" was found in the Docker image storage, but not in the Podman `
|
|
||||||
+ `image storage. The image(s) will be pulled into Podman image storage, pushed, and then `
|
|
||||||
+ `removed from the Podman image storage.`
|
|
||||||
);
|
|
||||||
isImageFromDocker = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
core.info(
|
|
||||||
`Tag "${sourceImages[0]}" was found in the Podman image storage, but not in the Docker `
|
|
||||||
+ `image storage. The image(s) will be pushed from Podman image storage.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let pushMsg = `⏳ Pushing "${sourceImages.join(", ")}" to "${destinationImages.join(", ")}" respectively`;
|
let pushMsg = `⏳ Pushing "${sourceImages.join(", ")}" to "${destinationImages.join(", ")}" respectively`;
|
||||||
if (username) {
|
if (username) {
|
||||||
|
@ -216,16 +221,25 @@ async function run(): Promise<void> {
|
||||||
|
|
||||||
// push the image
|
// push the image
|
||||||
for (let i = 0; i < destinationImages.length; i++) {
|
for (let i = 0; i < destinationImages.length; i++) {
|
||||||
const args = [
|
const args = [];
|
||||||
...(isImageFromDocker ? dockerPodmanOpts : []),
|
if (isImageFromDocker) {
|
||||||
|
args.push(...dockerPodmanOpts);
|
||||||
|
}
|
||||||
|
if (isManifest) {
|
||||||
|
args.push("manifest");
|
||||||
|
}
|
||||||
|
args.push(...[
|
||||||
"push",
|
"push",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
"--digestfile",
|
"--digestfile",
|
||||||
digestFile,
|
digestFile,
|
||||||
isImageFromDocker ? getFullDockerImageName(sourceImages[i]) : sourceImages[i],
|
isImageFromDocker ? getFullDockerImageName(sourceImages[i]) : sourceImages[i],
|
||||||
destinationImages[i],
|
destinationImages[i],
|
||||||
];
|
]);
|
||||||
|
// to push all the images referenced in the manifest
|
||||||
|
if (isManifest) {
|
||||||
|
args.push("--all");
|
||||||
|
}
|
||||||
if (podmanExtraArgs.length > 0) {
|
if (podmanExtraArgs.length > 0) {
|
||||||
args.push(...podmanExtraArgs);
|
args.push(...podmanExtraArgs);
|
||||||
}
|
}
|
||||||
|
@ -392,6 +406,39 @@ async function removeDockerPodmanImageStroage(): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkIfManifestsExists(): Promise<boolean> {
|
||||||
|
const foundManifests = [];
|
||||||
|
const missingManifests = [];
|
||||||
|
// check if manifest exist in Podman's storage
|
||||||
|
core.info(`🔍 Checking if the given image is manifest or not.`);
|
||||||
|
for (const manifest of sourceImages) {
|
||||||
|
const commandResult: ExecResult = await execute(
|
||||||
|
await getPodmanPath(),
|
||||||
|
[ "manifest", "exists", manifest ],
|
||||||
|
{ ignoreReturnCode: true, group: true }
|
||||||
|
);
|
||||||
|
if (commandResult.exitCode === 0) {
|
||||||
|
foundManifests.push(manifest);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
missingManifests.push(manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundManifests.length > 0) {
|
||||||
|
core.info(`Image${foundManifests.length !== 1 ? "s" : ""} "${foundManifests.join(", ")}" `
|
||||||
|
+ `${foundManifests.length !== 1 ? "are manifests" : "is a manifest"}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundManifests.length > 0 && missingManifests.length > 0) {
|
||||||
|
throw new Error(`Manifest${missingManifests.length !== 1 ? "s" : ""} "${missingManifests.join(", ")}" `
|
||||||
|
+ `not found in the Podman image storage. Make sure that all the provided images are either `
|
||||||
|
+ `manifests or container images.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundManifests.length === sourceImages.length;
|
||||||
|
}
|
||||||
|
|
||||||
async function execute(
|
async function execute(
|
||||||
executable: string,
|
executable: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
|
|
Loading…
Add table
Reference in a new issue