fix(kubernetes): make retained PVC preparation robust (#3697)
This commit is contained in:
@@ -3,6 +3,7 @@ package lifecycle
|
|||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const executorTypeSSH = "ssh"
|
const executorTypeSSH = "ssh"
|
||||||
@@ -12,6 +13,13 @@ const executorTypeSSH = "ssh"
|
|||||||
// user-authored prepare scripts must remain unchanged.
|
// user-authored prepare scripts must remain unchanged.
|
||||||
const legacySpritesPrepareScriptSHA256 = "e656f9e51496e1bb1e5cee205058bbeaa7fe1ab37073f600e6c94310ead7be4d"
|
const legacySpritesPrepareScriptSHA256 = "e656f9e51496e1bb1e5cee205058bbeaa7fe1ab37073f600e6c94310ead7be4d"
|
||||||
|
|
||||||
|
// These hashes identify scripts supplied by Kandev. Only exact matches receive
|
||||||
|
// managed fixes; customized scripts remain byte-for-byte unchanged.
|
||||||
|
const (
|
||||||
|
legacyKubernetesDefaultPrepareScriptSHA256 = "5fe05aedccc24565f338fc6d03f6814024aaf15655643b9ab6317986dfac0ce2"
|
||||||
|
legacyKubernetesFullPrepareScriptSHA256 = "540eece886af9577c6e9e652a845e373692c222453d62144cd6f03a4ab8bd734"
|
||||||
|
)
|
||||||
|
|
||||||
// DefaultPrepareScript returns the default prepare script for a given executor type string.
|
// DefaultPrepareScript returns the default prepare script for a given executor type string.
|
||||||
func DefaultPrepareScript(executorType string) string {
|
func DefaultPrepareScript(executorType string) string {
|
||||||
switch executorType {
|
switch executorType {
|
||||||
@@ -37,6 +45,41 @@ func isLegacySpritesPrepareScript(script string) bool {
|
|||||||
return hex.EncodeToString(digest[:]) == legacySpritesPrepareScriptSHA256
|
return hex.EncodeToString(digest[:]) == legacySpritesPrepareScriptSHA256
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func upgradeLegacyKubernetesPrepareScript(script string) string {
|
||||||
|
digest := sha256.Sum256([]byte(script))
|
||||||
|
switch hex.EncodeToString(digest[:]) {
|
||||||
|
case legacyKubernetesDefaultPrepareScriptSHA256:
|
||||||
|
return defaultKubernetesPrepareScript
|
||||||
|
case legacyKubernetesFullPrepareScriptSHA256:
|
||||||
|
upgraded := strings.Replace(script,
|
||||||
|
"clone_tmp=/opt/kandev/.workspace-clone\n",
|
||||||
|
"clone_tmp=/opt/kandev/.workspace-clone\n\n"+kubernetesRepositoryOriginNormalizer,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
upgraded = strings.Replace(upgraded, legacyKubernetesRepositoryOriginComparison,
|
||||||
|
currentKubernetesRepositoryOriginComparison, 1)
|
||||||
|
return strings.Replace(upgraded,
|
||||||
|
`cp -a "$clone_tmp"/. "$workspace"/`,
|
||||||
|
`cp -R "$clone_tmp"/. "$workspace"/`, 1)
|
||||||
|
default:
|
||||||
|
return script
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kubernetesRepositoryOriginNormalizer = `normalize_repository_origin() {
|
||||||
|
printf '%s\n' "$1" | sed \
|
||||||
|
-e 's|^https://[^/@]*@github.com/|https://github.com/|' \
|
||||||
|
-e 's|^git@github.com:|https://github.com/|' \
|
||||||
|
-e 's|^ssh://git@github.com/|https://github.com/|'
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const legacyKubernetesRepositoryOriginComparison = ` expected_origin=$(printf '%s\n' "$repository_url" | sed 's|^https://[^/@]*@github.com/|https://github.com/|')
|
||||||
|
retained_origin=$(printf '%s\n' "$workspace_origin" | sed 's|^https://[^/@]*@github.com/|https://github.com/|')`
|
||||||
|
|
||||||
|
const currentKubernetesRepositoryOriginComparison = ` expected_origin=$(normalize_repository_origin "$repository_url")
|
||||||
|
retained_origin=$(normalize_repository_origin "$workspace_origin")`
|
||||||
|
|
||||||
// KandevBranchCheckoutPostlude returns a kandev-managed shell snippet that
|
// KandevBranchCheckoutPostlude returns a kandev-managed shell snippet that
|
||||||
// guarantees the session's feature branch is checked out inside the
|
// guarantees the session's feature branch is checked out inside the
|
||||||
// workspace, no matter what the user's stored prepare_script does.
|
// workspace, no matter what the user's stored prepare_script does.
|
||||||
@@ -166,6 +209,13 @@ repository_url={{repository.clone_url}}
|
|||||||
repository_branch={{repository.branch}}
|
repository_branch={{repository.branch}}
|
||||||
clone_tmp=/opt/kandev/.workspace-clone
|
clone_tmp=/opt/kandev/.workspace-clone
|
||||||
|
|
||||||
|
normalize_repository_origin() {
|
||||||
|
printf '%s\n' "$1" | sed \
|
||||||
|
-e 's|^https://[^/@]*@github.com/|https://github.com/|' \
|
||||||
|
-e 's|^git@github.com:|https://github.com/|' \
|
||||||
|
-e 's|^ssh://git@github.com/|https://github.com/|'
|
||||||
|
}
|
||||||
|
|
||||||
# ---- Git identity and HTTPS authentication ----
|
# ---- Git identity and HTTPS authentication ----
|
||||||
{{git.identity_setup}}
|
{{git.identity_setup}}
|
||||||
git config --global --add safe.directory '*'
|
git config --global --add safe.directory '*'
|
||||||
@@ -182,8 +232,8 @@ if [ -n "$repository_url" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
workspace_origin=$(git -C "$workspace" remote get-url origin 2>/dev/null || true)
|
workspace_origin=$(git -C "$workspace" remote get-url origin 2>/dev/null || true)
|
||||||
expected_origin=$(printf '%s\n' "$repository_url" | sed 's|^https://[^/@]*@github.com/|https://github.com/|')
|
expected_origin=$(normalize_repository_origin "$repository_url")
|
||||||
retained_origin=$(printf '%s\n' "$workspace_origin" | sed 's|^https://[^/@]*@github.com/|https://github.com/|')
|
retained_origin=$(normalize_repository_origin "$workspace_origin")
|
||||||
if [ -z "$workspace_origin" ] || [ "$retained_origin" != "$expected_origin" ]; then
|
if [ -z "$workspace_origin" ] || [ "$retained_origin" != "$expected_origin" ]; then
|
||||||
echo 'kandev: retained workspace repository origin does not match the configured repository' >&2
|
echo 'kandev: retained workspace repository origin does not match the configured repository' >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -198,7 +248,7 @@ if [ -n "$repository_url" ]; then
|
|||||||
rm -rf "$clone_tmp"
|
rm -rf "$clone_tmp"
|
||||||
trap 'rm -rf "$clone_tmp"' 0 1 2 15
|
trap 'rm -rf "$clone_tmp"' 0 1 2 15
|
||||||
git clone --depth=1 --branch "$repository_branch" "$repository_url" "$clone_tmp"
|
git clone --depth=1 --branch "$repository_branch" "$repository_url" "$clone_tmp"
|
||||||
cp -a "$clone_tmp"/. "$workspace"/
|
cp -R "$clone_tmp"/. "$workspace"/
|
||||||
rm -rf "$clone_tmp"
|
rm -rf "$clone_tmp"
|
||||||
trap - 0 1 2 15
|
trap - 0 1 2 15
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ func TestDefaultPrepareScriptKubernetesReusesRetainedPVCWorkspace(t *testing.T)
|
|||||||
if err := os.MkdirAll(home, 0o700); err != nil {
|
if err := os.MkdirAll(home, 0o700); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
bin := filepath.Join(root, "bin")
|
||||||
|
if err := os.MkdirAll(bin, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
installMetadataRestrictedCopy(t, bin)
|
||||||
cloneTmp := filepath.Join(root, "runtime", "workspace-clone")
|
cloneTmp := filepath.Join(root, "runtime", "workspace-clone")
|
||||||
if err := os.MkdirAll(filepath.Dir(cloneTmp), 0o700); err != nil {
|
if err := os.MkdirAll(filepath.Dir(cloneTmp), 0o700); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -58,7 +63,7 @@ func TestDefaultPrepareScriptKubernetesReusesRetainedPVCWorkspace(t *testing.T)
|
|||||||
run := func() {
|
run := func() {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cmd := exec.Command("sh", "-eu", "-c", script)
|
cmd := exec.Command("sh", "-eu", "-c", script)
|
||||||
cmd.Env = append(os.Environ(), "HOME="+home)
|
cmd.Env = append(os.Environ(), "HOME="+home, "PATH="+bin+":"+os.Getenv("PATH"))
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("Kubernetes prepare failed: %v\n%s", err, output)
|
t.Fatalf("Kubernetes prepare failed: %v\n%s", err, output)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,6 +304,8 @@ func kubernetesPrepareScript(req *ExecutorCreateRequest) (string, error) {
|
|||||||
script := getMetadataString(req.Metadata, MetadataKeySetupScript)
|
script := getMetadataString(req.Metadata, MetadataKeySetupScript)
|
||||||
if script == "" {
|
if script == "" {
|
||||||
script = DefaultPrepareScript("k8s")
|
script = DefaultPrepareScript("k8s")
|
||||||
|
} else {
|
||||||
|
script = upgradeLegacyKubernetesPrepareScript(script)
|
||||||
}
|
}
|
||||||
if script == "" {
|
if script == "" {
|
||||||
return ":\n", nil
|
return ":\n", nil
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ func TestFullWorkerPreparationClonesBeforeCachesAndRetainsWorkspace(t *testing.T
|
|||||||
if err := os.MkdirAll(bin, 0755); err != nil {
|
if err := os.MkdirAll(bin, 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
installMetadataRestrictedCopy(t, bin)
|
||||||
if err := os.WriteFile(filepath.Join(bin, "docker"), []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
|
if err := os.WriteFile(filepath.Join(bin, "docker"), []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func installMetadataRestrictedCopy(t *testing.T, bin string) {
|
||||||
|
t.Helper()
|
||||||
|
realCopy, err := exec.LookPath("cp")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wrapper := `#!/bin/sh
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
-a|-p|--archive|--preserve|--preserve=*)
|
||||||
|
echo 'cp: preserving metadata: Operation not permitted' >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec ` + shellQuote(realCopy) + ` "$@"
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(bin, "cp"), []byte(wrapper), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKubernetesPreparationAcceptsEquivalentGitHubOrigins(t *testing.T) {
|
||||||
|
full, err := os.ReadFile("../../../../../../k8s/worker-images/full/prepare.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for name, script := range map[string]string{
|
||||||
|
"default": DefaultPrepareScript("k8s"),
|
||||||
|
"full": string(full),
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
runKubernetesPreparationWithEquivalentGitHubOrigins(t, script, name == "full")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKubernetesPrepareScriptUpgradesPersistedManagedScripts(t *testing.T) {
|
||||||
|
full, err := os.ReadFile("../../../../../../k8s/worker-images/full/prepare.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
legacyDefault := persistedKubernetesPrepareScriptBeforePVCFix(t, DefaultPrepareScript("k8s"))
|
||||||
|
for name, current := range map[string]string{
|
||||||
|
"default": DefaultPrepareScript("k8s"),
|
||||||
|
"full": string(full),
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
legacy := persistedKubernetesPrepareScriptBeforePVCFix(t, current)
|
||||||
|
if upgraded := upgradeLegacyKubernetesPrepareScript(legacy); upgraded != current {
|
||||||
|
t.Fatalf("managed script upgrade did not produce the current script")
|
||||||
|
}
|
||||||
|
req := validKubernetesCreateRequest()
|
||||||
|
req.Metadata[MetadataKeySetupScript] = legacy
|
||||||
|
|
||||||
|
resolved, err := kubernetesPrepareScript(req)
|
||||||
|
|
||||||
|
requireNoError(t, err)
|
||||||
|
if !strings.Contains(resolved, "normalize_repository_origin()") ||
|
||||||
|
!strings.Contains(resolved, `cp -R "$clone_tmp"/. "$workspace"/`) {
|
||||||
|
t.Fatalf("persisted managed script was not upgraded:\n%s", resolved)
|
||||||
|
}
|
||||||
|
if strings.Contains(resolved, `cp -a "$clone_tmp"/. "$workspace"/`) {
|
||||||
|
t.Fatalf("persisted managed script retained archive copy:\n%s", resolved)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const customization = "# user-managed customization"
|
||||||
|
req := validKubernetesCreateRequest()
|
||||||
|
req.Metadata[MetadataKeySetupScript] = legacyDefault + "\n" + customization
|
||||||
|
resolved, err := kubernetesPrepareScript(req)
|
||||||
|
requireNoError(t, err)
|
||||||
|
if !strings.Contains(resolved, customization) ||
|
||||||
|
!strings.Contains(resolved, `cp -a "$clone_tmp"/. "$workspace"/`) ||
|
||||||
|
strings.Contains(resolved, "normalize_repository_origin()") {
|
||||||
|
t.Fatalf("custom prepare script was modified:\n%s", resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistedKubernetesPrepareScriptBeforePVCFix(t *testing.T, current string) string {
|
||||||
|
t.Helper()
|
||||||
|
legacy := strings.Replace(current, `
|
||||||
|
normalize_repository_origin() {
|
||||||
|
printf '%s\n' "$1" | sed \
|
||||||
|
-e 's|^https://[^/@]*@github.com/|https://github.com/|' \
|
||||||
|
-e 's|^git@github.com:|https://github.com/|' \
|
||||||
|
-e 's|^ssh://git@github.com/|https://github.com/|'
|
||||||
|
}
|
||||||
|
`, "", 1)
|
||||||
|
legacy = strings.Replace(legacy, ` expected_origin=$(normalize_repository_origin "$repository_url")
|
||||||
|
retained_origin=$(normalize_repository_origin "$workspace_origin")`, ` expected_origin=$(printf '%s\n' "$repository_url" | sed 's|^https://[^/@]*@github.com/|https://github.com/|')
|
||||||
|
retained_origin=$(printf '%s\n' "$workspace_origin" | sed 's|^https://[^/@]*@github.com/|https://github.com/|')`, 1)
|
||||||
|
legacy = strings.Replace(legacy,
|
||||||
|
`cp -R "$clone_tmp"/. "$workspace"/`,
|
||||||
|
`cp -a "$clone_tmp"/. "$workspace"/`, 1)
|
||||||
|
if legacy == current {
|
||||||
|
t.Fatal("legacy fixture did not reverse the managed PVC preparation changes")
|
||||||
|
}
|
||||||
|
return legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKubernetesPreparationWithEquivalentGitHubOrigins(t *testing.T, script string, needsDocker bool) {
|
||||||
|
t.Helper()
|
||||||
|
workspace, _ := setupPostludeRepo(t, "main")
|
||||||
|
runIn(t, workspace, "git", "remote", "set-url", "origin", "https://github.com/acme/repo.git")
|
||||||
|
marker := filepath.Join(workspace, "retained.txt")
|
||||||
|
requireNoError(t, os.WriteFile(marker, []byte("preserve me"), 0o600))
|
||||||
|
root := t.TempDir()
|
||||||
|
home := filepath.Join(root, "home")
|
||||||
|
bin := filepath.Join(root, "bin")
|
||||||
|
requireNoError(t, os.MkdirAll(home, 0o700))
|
||||||
|
requireNoError(t, os.MkdirAll(bin, 0o755))
|
||||||
|
if needsDocker {
|
||||||
|
requireNoError(t, os.WriteFile(filepath.Join(bin, "docker"), []byte("#!/bin/sh\nexit 0\n"), 0o755))
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered := strings.NewReplacer(
|
||||||
|
"{{git.identity_setup}}", ":",
|
||||||
|
"{{github.auth_setup}}", ":",
|
||||||
|
"{{repository.branch}}", shellQuote("main"),
|
||||||
|
"{{repository.clone_url}}", shellQuote("git@github.com:acme/repo.git"),
|
||||||
|
"{{workspace.path}}", shellQuote(workspace),
|
||||||
|
"{{repository.setup_script}}", ":",
|
||||||
|
"{{kandev.agents.install}}", ":",
|
||||||
|
"/opt/kandev/.workspace-clone", filepath.Join(root, "runtime", "workspace-clone"),
|
||||||
|
).Replace(script)
|
||||||
|
cmd := exec.Command("sh", "-eu", "-c", rendered)
|
||||||
|
cmd.Env = append(os.Environ(), "HOME="+home, "PATH="+bin+":"+os.Getenv("PATH"))
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("Kubernetes prepare rejected equivalent GitHub origins: %v\n%s", err, output)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(marker)
|
||||||
|
if err != nil || string(data) != "preserve me" {
|
||||||
|
t.Fatalf("retained workspace data = %q, %v", data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,3 +134,17 @@ The exact implementation revision was exercised with image config digest
|
|||||||
Required Kubernetes and lifecycle Go packages, changed-file ESLint and test
|
Required Kubernetes and lifecycle Go packages, changed-file ESLint and test
|
||||||
discovery also passed. This evidence applies to the recorded Kind/runtime
|
discovery also passed. This evidence applies to the recorded Kind/runtime
|
||||||
combination; other container runtimes still require their own cgroup validation.
|
combination; other container runtimes still require their own cgroup validation.
|
||||||
|
|
||||||
|
A later Kubernetes acceptance run exposed two retained-workspace portability
|
||||||
|
gaps. A non-root workload could copy a checkout onto a group-writable,
|
||||||
|
root-owned PVC and then fail when `cp -a` restored metadata on the mount root.
|
||||||
|
The preparation scripts now copy checkout contents recursively without
|
||||||
|
restoring source ownership or timestamps. The same run also showed that a
|
||||||
|
credential-free GitHub HTTPS origin and the equivalent GitHub SSH origin were
|
||||||
|
compared literally during lost-Pod replacement. Preparation now normalizes
|
||||||
|
those GitHub URL forms before comparison while continuing to reject a different
|
||||||
|
repository. Runtime resolution upgrades exact persisted copies of the two
|
||||||
|
Kandev-supplied scripts; any customized script remains byte-for-byte unchanged.
|
||||||
|
Focused lifecycle regressions exercise both the built-in Kubernetes script and
|
||||||
|
the full worker script, preserve retained files, and reproduce the restricted
|
||||||
|
copy boundary with the real `cp` implementation.
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ repository_url={{repository.clone_url}}
|
|||||||
repository_branch={{repository.branch}}
|
repository_branch={{repository.branch}}
|
||||||
clone_tmp=/opt/kandev/.workspace-clone
|
clone_tmp=/opt/kandev/.workspace-clone
|
||||||
|
|
||||||
|
normalize_repository_origin() {
|
||||||
|
printf '%s\n' "$1" | sed \
|
||||||
|
-e 's|^https://[^/@]*@github.com/|https://github.com/|' \
|
||||||
|
-e 's|^git@github.com:|https://github.com/|' \
|
||||||
|
-e 's|^ssh://git@github.com/|https://github.com/|'
|
||||||
|
}
|
||||||
|
|
||||||
# ---- Git identity and HTTPS authentication ----
|
# ---- Git identity and HTTPS authentication ----
|
||||||
{{git.identity_setup}}
|
{{git.identity_setup}}
|
||||||
git config --global --add safe.directory '*'
|
git config --global --add safe.directory '*'
|
||||||
@@ -44,8 +51,8 @@ if [ -n "$repository_url" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
workspace_origin=$(git -C "$workspace" remote get-url origin 2>/dev/null || true)
|
workspace_origin=$(git -C "$workspace" remote get-url origin 2>/dev/null || true)
|
||||||
expected_origin=$(printf '%s\n' "$repository_url" | sed 's|^https://[^/@]*@github.com/|https://github.com/|')
|
expected_origin=$(normalize_repository_origin "$repository_url")
|
||||||
retained_origin=$(printf '%s\n' "$workspace_origin" | sed 's|^https://[^/@]*@github.com/|https://github.com/|')
|
retained_origin=$(normalize_repository_origin "$workspace_origin")
|
||||||
if [ -z "$workspace_origin" ] || [ "$retained_origin" != "$expected_origin" ]; then
|
if [ -z "$workspace_origin" ] || [ "$retained_origin" != "$expected_origin" ]; then
|
||||||
echo 'kandev: retained workspace repository origin does not match the configured repository' >&2
|
echo 'kandev: retained workspace repository origin does not match the configured repository' >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -60,7 +67,7 @@ if [ -n "$repository_url" ]; then
|
|||||||
rm -rf "$clone_tmp"
|
rm -rf "$clone_tmp"
|
||||||
trap 'rm -rf "$clone_tmp"' 0 1 2 15
|
trap 'rm -rf "$clone_tmp"' 0 1 2 15
|
||||||
git clone --depth=1 --branch "$repository_branch" "$repository_url" "$clone_tmp"
|
git clone --depth=1 --branch "$repository_branch" "$repository_url" "$clone_tmp"
|
||||||
cp -a "$clone_tmp"/. "$workspace"/
|
cp -R "$clone_tmp"/. "$workspace"/
|
||||||
rm -rf "$clone_tmp"
|
rm -rf "$clone_tmp"
|
||||||
trap - 0 1 2 15
|
trap - 0 1 2 15
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user