...
 
Commits (2)
......@@ -271,7 +271,7 @@ type RunnerSettings struct {
PreBuildScript string `toml:"pre_build_script,omitempty" json:"pre_build_script" long:"pre-build-script" env:"RUNNER_PRE_BUILD_SCRIPT" description:"Runner-specific command script executed after code is pulled, just before build executes"`
PostBuildScript string `toml:"post_build_script,omitempty" json:"post_build_script" long:"post-build-script" env:"RUNNER_POST_BUILD_SCRIPT" description:"Runner-specific command script executed after code is pulled and just after build executes"`
Shell string `toml:"shell,omitempty" json:"shell" long:"shell" env:"RUNNER_SHELL" description:"Select bash, cmd or powershell"`
Shell string `toml:"shell,omitempty" json:"shell" long:"shell" env:"RUNNER_SHELL" description:"Select bash, cmd, pwsh or powershell"`
SSH *ssh.Config `toml:"ssh,omitempty" json:"ssh" group:"ssh executor" namespace:"ssh"`
Docker *DockerConfig `toml:"docker,omitempty" json:"docker" group:"docker executor" namespace:"docker"`
......
......@@ -170,6 +170,7 @@ There are a couple of available shells that can be run on different platforms.
| `sh` | generate Sh (Bourne-shell) script. All commands executed in Sh context (fallback for `bash` for all Unix systems) |
| `cmd` | generate Windows Batch script. All commands are executed in Batch context (default for Windows) |
| `powershell` | generate Windows PowerShell script. All commands are executed in PowerShell context |
| `pwsh` | generate Windows PowerShell Core script. All commands are executed in Powershell Core context |
## The `[runners.docker]` section
......
......@@ -131,6 +131,7 @@ These are the binaries that you can install:
[Minikube](https://github.com/kubernetes/minikube)
1. [Parallels](http://www.parallels.com/products/desktop/download/)
1. [PowerShell](https://msdn.microsoft.com/en-us/powershell)
1. [PowerShell Core](https://docs.microsoft.com/en-us/powershell/scripting/setup/installing-powershell-core-on-windows?view=powershell-6)
After installing the binaries run:
......
......@@ -99,20 +99,20 @@ Supported features by different executors:
Supported systems by different shells:
| Shells | Bash | Windows Batch | PowerShell |
|:-------:|:-----------:|:-------------:|:----------:|
| Windows | ✓ | ✓ (default) | ✓ |
| Linux | ✓ (default) | ✗ | ✗ |
| OSX | ✓ (default) | ✗ | ✗ |
| FreeBSD | ✓ (default) | ✗ | ✗ |
| Shells | Bash | Windows Batch | PowerShell | PowerShell Core |
|:-------:|:-----------:|:-------------:|:----------:|:---------------:|
| Windows | ✓ | ✓ (default) | ✓ | ✓ |
| Linux | ✓ (default) | ✗ | ✗ | ✗ |
| OSX | ✓ (default) | ✗ | ✗ | ✗ |
| FreeBSD | ✓ (default) | ✗ | ✗ | ✗ |
Supported systems for interactive web terminals by different shells:
| Shells | Bash | Windows Batch | PowerShell |
|:-------:|:-----------:|:-------------:|:----------:|
| Windows | ✗ | ✗ | ✗ |
| Linux | ✓ | ✗ | ✗ |
| OSX | ✓ | ✗ | ✗ |
| FreeBSD | ✓ | ✗ | ✗ |
| Shells | Bash | Windows Batch | PowerShell | PowerShell Core |
|:-------:|:-----------:|:-------------:|:----------:|:---------------:|
| Windows | ✗ | ✗ | ✗ | ✗ |
| Linux | ✓ | ✗ | ✗ | ✗ |
| OSX | ✓ | ✗ | ✗ | ✗ |
| FreeBSD | ✓ | ✗ | ✗ | ✗ |
[services]: https://docs.gitlab.com/ee/ci/services/README.html
......@@ -3,7 +3,7 @@
The Shell executor is a simple executor that allows you to execute builds
locally to the machine that the Runner is installed. It supports all systems on
which the Runner can be installed. That means that it's possible to use scripts
generated for Bash, Windows PowerShell and Windows Batch.
generated for Bash, Windows PowerShell, Windows PowerShell Core, and Windows Batch.
## Overview
......
......@@ -38,7 +38,7 @@ installed.
- using Docker containers with autoscaling on different clouds and virtualization hypervisors
- connecting to remote SSH server
- Is written in Go and distributed as single binary without any other requirements
- Supports Bash, Windows Batch and Windows PowerShell
- Supports Bash, Windows Batch, Windows PowerShell, and Windows PowerShell Core
- Works on GNU/Linux, OS X and Windows (pretty much anywhere you can run Docker)
- Allows to customize the job running environment
- Automatic configuration reload without restart
......
......@@ -48,7 +48,7 @@ want to install a version prior to GitLab Runner 10, [visit the old docs](old.md
1. (Optional) Update Runners `concurrent` value in `C:\GitLab-Runner\config.toml`
to allow multiple concurrent jobs as detailed in [advanced configuration details](../configuration/advanced-configuration.md).
Additionally you can use the advanced configuration details to update your
shell executor to use Bash or PowerShell rather than Batch.
shell executor to use Bash, PowerShell, or PowerShell Core rather than Batch.
Voila! Runner is installed, running, and will start again after each system reboot.
Logs are stored in Windows Event Log.
......
......@@ -24,6 +24,7 @@ The currently supported shells are:
| `sh` | Sh (Bourne-shell) shell. All commands executed in Sh context (fallback for `bash` for all Unix systems) |
| `cmd` | Windows Batch script. All commands are executed in Batch context (default for Windows) |
| `powershell` | Windows PowerShell script. All commands are executed in PowerShell context |
| `pwsh` | WindowS PowerShell Core script. All commands are executed in PowerShell Core context |
## Sh/Bash shells
......@@ -182,7 +183,7 @@ IF %errorlevel% NEQ 0 exit /b %errorlevel%
goto :EOF
```
## PowerShell
## PowerShell/PowerShell Core
PowerShell doesn't support executing the build in context of another user.
......
package shells
import (
"bufio"
"bytes"
"fmt"
"io"
"path"
"path/filepath"
"strings"
"gitlab.com/gitlab-org/gitlab-runner/common"
"gitlab.com/gitlab-org/gitlab-runner/helpers"
)
type Pwsh struct {
AbstractShell
}
type PwshWriter struct {
bytes.Buffer
TemporaryPath string
indent int
}
func pwshQuote(text string) string {
// taken from: http://www.robvanderwoude.com/escapechars.php
text = strings.Replace(text, "`", "``", -1)
// text = strings.Replace(text, "\0", "`0", -1)
text = strings.Replace(text, "\a", "`a", -1)
text = strings.Replace(text, "\b", "`b", -1)
text = strings.Replace(text, "\f", "^f", -1)
text = strings.Replace(text, "\r", "`r", -1)
text = strings.Replace(text, "\n", "`n", -1)
text = strings.Replace(text, "\t", "^t", -1)
text = strings.Replace(text, "\v", "^v", -1)
text = strings.Replace(text, "#", "`#", -1)
text = strings.Replace(text, "'", "`'", -1)
text = strings.Replace(text, "\"", "`\"", -1)
return "\"" + text + "\""
}
func pwshQuoteVariable(text string) string {
text = pwshQuote(text)
text = strings.Replace(text, "$", "`$", -1)
return text
}
func (b *PwshWriter) GetTemporaryPath() string {
return b.TemporaryPath
}
func (b *PwshWriter) Line(text string) {
b.WriteString(strings.Repeat(" ", b.indent) + text + "\r\n")
}
func (b *PwshWriter) CheckForErrors() {
b.checkErrorLevel()
}
func (b *PwshWriter) Indent() {
b.indent++
}
func (b *PwshWriter) Unindent() {
b.indent--
}
func (b *PwshWriter) checkErrorLevel() {
b.Line("if(!$?) { Exit $LASTEXITCODE }")
b.Line("")
}
func (b *PwshWriter) Command(command string, arguments ...string) {
b.Line(b.buildCommand(command, arguments...))
b.checkErrorLevel()
}
func (b *PwshWriter) buildCommand(command string, arguments ...string) string {
list := []string{
pwshQuote(command),
}
for _, argument := range arguments {
list = append(list, pwshQuote(argument))
}
return "& " + strings.Join(list, " ")
}
func (b *PwshWriter) TmpFile(name string) string {
filePath := b.Absolute(path.Join(b.TemporaryPath, name))
return helpers.ToBackslash(filePath)
}
func (b *PwshWriter) EnvVariableKey(name string) string {
return fmt.Sprintf("$%s", name)
}
func (b *PwshWriter) Variable(variable common.JobVariable) {
if variable.File {
variableFile := b.TmpFile(variable.Key)
b.Line(fmt.Sprintf("md %s -Force | out-null", pwshQuote(helpers.ToBackslash(b.TemporaryPath))))
b.Line(fmt.Sprintf("Set-Content %s -Value %s -Encoding UTF8 -Force", pwshQuote(variableFile), pwshQuoteVariable(variable.Value)))
b.Line("$" + variable.Key + "=" + pwshQuote(variableFile))
} else {
b.Line("$" + variable.Key + "=" + pwshQuoteVariable(variable.Value))
}
b.Line("$env:" + variable.Key + "=$" + variable.Key)
}
func (b *PwshWriter) IfDirectory(path string) {
b.Line("if(Test-Path " + pwshQuote(helpers.ToBackslash(path)) + " -PathType Container) {")
b.Indent()
}
func (b *PwshWriter) IfFile(path string) {
b.Line("if(Test-Path " + pwshQuote(helpers.ToBackslash(path)) + " -PathType Leaf) {")
b.Indent()
}
func (b *PwshWriter) IfCmd(cmd string, arguments ...string) {
b.Line(b.buildCommand(cmd, arguments...) + " 2>$null")
b.Line("if($?) {")
b.Indent()
}
func (b *PwshWriter) IfCmdWithOutput(cmd string, arguments ...string) {
b.Line(b.buildCommand(cmd, arguments...))
b.Line("if($?) {")
b.Indent()
}
func (b *PwshWriter) Else() {
b.Unindent()
b.Line("} else {")
b.Indent()
}
func (b *PwshWriter) EndIf() {
b.Unindent()
b.Line("}")
}
func (b *PwshWriter) Cd(path string) {
b.Line("cd " + pwshQuote(helpers.ToBackslash(path)))
b.checkErrorLevel()
}
func (b *PwshWriter) MkDir(path string) {
b.Line(fmt.Sprintf("md %s -Force | out-null", pwshQuote(helpers.ToBackslash(path))))
}
func (b *PwshWriter) MkTmpDir(name string) string {
path := helpers.ToBackslash(path.Join(b.TemporaryPath, name))
b.MkDir(path)
return path
}
func (b *PwshWriter) RmDir(path string) {
path = pwshQuote(helpers.ToBackslash(path))
b.Line("if( (Get-Command -Name Remove-Item2 -Module NTFSSecurity -ErrorAction SilentlyContinue) -and (Test-Path " + path + " -PathType Container) ) {")
b.Indent()
b.Line("Remove-Item2 -Force -Recurse " + path)
b.Unindent()
b.Line("} elseif(Test-Path " + path + ") {")
b.Indent()
b.Line("Remove-Item -Force -Recurse " + path)
b.Unindent()
b.Line("}")
b.Line("")
}
func (b *PwshWriter) RmFile(path string) {
path = pwshQuote(helpers.ToBackslash(path))
b.Line("if( (Get-Command -Name Remove-Item2 -Module NTFSSecurity -ErrorAction SilentlyContinue) -and (Test-Path " + path + " -PathType Leaf) ) {")
b.Indent()
b.Line("Remove-Item2 -Force " + path)
b.Unindent()
b.Line("} elseif(Test-Path " + path + ") {")
b.Indent()
b.Line("Remove-Item -Force " + path)
b.Unindent()
b.Line("}")
b.Line("")
}
func (b *PwshWriter) Print(format string, arguments ...interface{}) {
coloredText := helpers.ANSI_RESET + fmt.Sprintf(format, arguments...)
b.Line("echo " + pwshQuoteVariable(coloredText))
}
func (b *PwshWriter) Notice(format string, arguments ...interface{}) {
coloredText := helpers.ANSI_BOLD_GREEN + fmt.Sprintf(format, arguments...) + helpers.ANSI_RESET
b.Line("echo " + pwshQuoteVariable(coloredText))
}
func (b *PwshWriter) Warning(format string, arguments ...interface{}) {
coloredText := helpers.ANSI_YELLOW + fmt.Sprintf(format, arguments...) + helpers.ANSI_RESET
b.Line("echo " + pwshQuoteVariable(coloredText))
}
func (b *PwshWriter) Error(format string, arguments ...interface{}) {
coloredText := helpers.ANSI_BOLD_RED + fmt.Sprintf(format, arguments...) + helpers.ANSI_RESET
b.Line("echo " + pwshQuoteVariable(coloredText))
}
func (b *PwshWriter) EmptyLine() {
b.Line("echo \"\"")
}
func (b *PwshWriter) Absolute(dir string) string {
if filepath.IsAbs(dir) {
return dir
}
b.Line("$CurrentDirectory = (Resolve-Path .\\).Path")
return filepath.Join("$CurrentDirectory", dir)
}
func (b *PwshWriter) Finish(trace bool) string {
var buffer bytes.Buffer
w := bufio.NewWriter(&buffer)
// write BOM
io.WriteString(w, "\xef\xbb\xbf")
if trace {
io.WriteString(w, "Set-PSDebug -Trace 2\r\n")
}
io.WriteString(w, b.String())
w.Flush()
return buffer.String()
}
func (b *Pwsh) GetName() string {
return "pwsh"
}
func (b *Pwsh) GetConfiguration(info common.ShellScriptInfo) (script *common.ShellConfiguration, err error) {
script = &common.ShellConfiguration{
Command: "pwsh",
Arguments: []string{"-noprofile", "-noninteractive", "-executionpolicy", "Bypass", "-command"},
PassFile: true,
Extension: "ps1",
}
return
}
func (b *Pwsh) GenerateScript(buildStage common.BuildStage, info common.ShellScriptInfo) (script string, err error) {
w := &PwshWriter{
TemporaryPath: info.Build.FullProjectDir() + ".tmp",
}
if buildStage == common.BuildStagePrepare {
if len(info.Build.Hostname) != 0 {
w.Line("echo \"Running on $env:computername via " + pwshQuoteVariable(info.Build.Hostname) + "...\"")
} else {
w.Line("echo \"Running on $env:computername...\"")
}
}
err = b.writeScript(w, buildStage, info)
script = w.Finish(info.Build.IsDebugTraceEnabled())
return
}
func (b *Pwsh) IsDefault() bool {
return false
}
func init() {
common.RegisterShell(&Pwsh{})
}
package shells
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPwsh_CommandShellEscapes(t *testing.T) {
writer := &PwshWriter{}
writer.Command("foo", "x&(y)")
assert.Equal(t, "& \"foo\" \"x&(y)\"\r\nif(!$?) { Exit $LASTEXITCODE }\r\n\r\n", writer.String())
}
func TestPwsh_IfCmdShellEscapes(t *testing.T) {
writer := &PwshWriter{}
writer.IfCmd("foo", "x&(y)")
assert.Equal(t, "& \"foo\" \"x&(y)\" 2>$null\r\nif($?) {\r\n", writer.String())
}
......@@ -58,4 +58,5 @@ func TestMkDir(t *testing.T) {
onShell(t, "bash", "bash", "sh", []string{}, &BashWriter{TemporaryPath: tmpDir})
onShell(t, "cmd", "cmd.exe", "cmd", []string{"/Q", "/C"}, &CmdWriter{TemporaryPath: tmpDir})
onShell(t, "powershell", "powershell.exe", "ps1", []string{"-noprofile", "-noninteractive", "-executionpolicy", "Bypass", "-command"}, &PsWriter{TemporaryPath: tmpDir})
onShell(t, "pwsh", "pwsh.exe", "ps1", []string{"-noprofile", "-noninteractive", "-executionpolicy", "Bypass", "-command"}, &PwshWriter{TemporaryPath: tmpDir})
}
......@@ -237,7 +237,7 @@ var Commands = []cli.Command{
},
cli.StringFlag{
Name: "shell",
Usage: "Force environment to be configured for a specified shell: [fish, cmd, powershell, tcsh], default is auto-detect",
Usage: "Force environment to be configured for a specified shell: [fish, cmd, pwsh, powershell, tcsh], default is auto-detect",
},
cli.BoolFlag{
Name: "unset, u",
......
......@@ -137,6 +137,10 @@ func shellCfgSet(c CommandLine, api libmachine.API) (*ShellConfig, error) {
shellCfg.Prefix = "$Env:"
shellCfg.Suffix = "\"\n"
shellCfg.Delimiter = " = \""
case "pwsh":
shellCfg.Prefix = "$Env:"
shellCfg.Suffix = "\"\n"
shellCfg.Delimiter = " = \""
case "cmd":
shellCfg.Prefix = "SET "
shellCfg.Suffix = "\n"
......@@ -185,6 +189,10 @@ func shellCfgUnset(c CommandLine, api libmachine.API) (*ShellConfig, error) {
shellCfg.Prefix = `Remove-Item Env:\\`
shellCfg.Suffix = "\n"
shellCfg.Delimiter = ""
case "pwsh":
shellCfg.Prefix = `Remove-Item Env:\\`
shellCfg.Suffix = "\n"
shellCfg.Delimiter = ""
case "cmd":
shellCfg.Prefix = "SET "
shellCfg.Suffix = "\n"
......@@ -258,6 +266,8 @@ func (g *EnvUsageHintGenerator) GenerateUsageHint(userShell string, args []strin
cmd = fmt.Sprintf("eval (%s)", commandLine)
case "powershell":
cmd = fmt.Sprintf("& %s | Invoke-Expression", commandLine)
case "pwsh":
cmd = fmt.Sprintf("& %s | Invoke-Expression", commandLine)
case "cmd":
cmd = fmt.Sprintf("\t@FOR /f \"tokens=*\" %%i IN ('%s') DO @%%i", commandLine)
comment = "REM"
......
......@@ -156,7 +156,7 @@ func (d *Driver) GetState() (state.State, error) {
// PreCreateCheck checks that the machine creation process can be started safely.
func (d *Driver) PreCreateCheck() error {
// Check that powershell was found
if powershell == "" {
if (powershell == "") && (pwsh == "") {
return ErrPowerShellNotFound
}
......
......@@ -13,21 +13,27 @@ import (
)
var powershell string
var pwsh string
var (
ErrPowerShellNotFound = errors.New("Powershell was not found in the path")
ErrPowerShellNotFound = errors.New("Neither PowerShell or Pwsh were found in the path")
ErrNotAdministrator = errors.New("Hyper-v commands have to be run as an Administrator")
ErrNotInstalled = errors.New("Hyper-V PowerShell Module is not available")
)
func init() {
powershell, _ = exec.LookPath("powershell.exe")
pwsh, _ = exec.LookPath("pwsh.exe")
}
func cmdOut(args ...string) (string, error) {
args = append([]string{"-NoProfile", "-NonInteractive"}, args...)
cmd := exec.Command(powershell, args...)
log.Debugf("[executing ==>] : %v %v", powershell, strings.Join(args, " "))
myshell := powershell
if pwsh != "" {
myshell = pwsh
}
cmd := exec.Command(myshell, args...)
log.Debugf("[executing ==>] : %v %v", myshell, strings.Join(args, " "))
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
......
......@@ -58,6 +58,8 @@ func Detect() (string, error) {
}
if strings.Contains(strings.ToLower(shell), "powershell") {
return "powershell", nil
else if strings.Contains(strings.ToLower(shell), "pwsh") {
return "pwsh", nil
} else if strings.Contains(strings.ToLower(shell), "cmd") {
return "cmd", nil
} else {
......@@ -67,6 +69,8 @@ func Detect() (string, error) {
}
if strings.Contains(strings.ToLower(shell), "powershell") {
return "powershell", nil
} else if strings.Contains(strings.ToLower(shell), "pwsh") {
return "pwsh", nil
} else if strings.Contains(strings.ToLower(shell), "cmd") {
return "cmd", nil
} else {
......