Skip to content

Commit

Permalink
after-script feature
Browse files Browse the repository at this point in the history
add support for after-script pipeline entry. an after-script is always
executed after the script with the BITBUCKET_EXIT_CODE environment
variable set to the scripts last executed command exit status.
  • Loading branch information
ktomk committed Apr 11, 2020
1 parent 31ff473 commit d1dea9b
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 24 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [unreleased]
### Added
- Support for `after-script:` incl. `BITBUCKET_EXIT_CODE` environment parameter.
### Fixed
- Corrections in Read Me and Change Log

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ to use the development version.
- max-time (never needed this)
- size (likely neglected for local run, limited support for
[Rootless Pipelines](doc/PIPELINES-HOWTO-ROOTLESS.md))
- [ ] step.after-script (*after-script* feature)
- [X] step.after-script (*after-script* feature)
- [ ] Get VCS revision from working directory (*git-deployment* feature)
- [ ] Use a different project directory `--project-dir <path>` to
specify the root path to deploy into the container, which
Expand Down
14 changes: 14 additions & 0 deletions bitbucket-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,17 @@ pipelines:
name: print environemnt variables
script:
- printenv | sort
after-script:
- step:
name: Happy
script:
- echo "fine"
after-script:
- 'echo "BITBUCKET_EXIT_CODE: ${BITBUCKET_EXIT_CODE}"'
- step:
name: Broken
script:
- exit 123
- echo "already broken"
after-script:
- 'echo "BITBUCKET_EXIT_CODE: ${BITBUCKET_EXIT_CODE}"'
7 changes: 1 addition & 6 deletions doc/PIPELINES-VARIABLE-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ fashion (façon).
| `BITBUCKET_COMMIT` | *all* options; always set to "`0000000000000000000000000000000000000000`" |
| `BITBUCKET_DEPLOYMENT_ENVIRONMENT` | -/-; currently unsupported |
| `BITBUCKET_DEPLOYMENT_ENVIRONMENT_UUID` | -/-; currently unsupported |
| `BITBUCKET_EXIT_CODE` | -/-; currently unsupported; *after-script* feature |
| `BITBUCKET_EXIT_CODE` | *all* options; set to the exit status of the `script` for use in the `after-script` |
| `BITBUCKET_GIT_HTTP_ORIGIN` | -/-; currently unsupported |
| `BITBUCKET_GIT_SSH_ORIGIN` | -/-; currently unsupported |
| `BITBUCKET_PARALLEL_STEP` | *all* options; in a parallel step set to zero-based index of the current step in the group, e.g. 0, 1, 2, ... |
Expand Down Expand Up @@ -67,11 +67,6 @@ the depployments are not (yet) supported, therefore the variable
call makes not much sense as it would be set for every script,
and not the deployment script only.

Another example is `BITBUCKET_EXIT_CODE`. Even thought this
variable is easy to be set by the current piplines runner
implementation, there is no such *after-script* handling feature
yet where that variable is of practical use.

However:

Some variables which are valid per project and which are marked
Expand Down
4 changes: 4 additions & 0 deletions lib/pipelines/environment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ BITBUCKET_CLONE_DIR The absolute path of the directory that the \
container.
BITBUCKET_COMMIT The commit hash of a commit that kicked off the \
build.
BITBUCKET_EXIT_CODE The exit status of the last executed command from \
a script. If a script command returns non-zero \
it is the last executed command. \
Set in the after-script.
BITBUCKET_PR_DESTINATION_BRANCH \
The name of the pull request destination branch \
in combination with BITBUCKET_BRANCH available on \
Expand Down
40 changes: 35 additions & 5 deletions src/File/Pipeline/Step.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ public function __construct(Pipeline $pipeline, $index, array $step, array $env
// quick validation: trigger
$this->validateTrigger($step, (bool)$env);

// quick validation: script
// quick validation: script + after-script
$this->parseScript($step);
$this->parseAfterScript($step);

$this->pipeline = $pipeline;
$this->index = $index;
Expand Down Expand Up @@ -108,6 +109,18 @@ public function getScript()
return $this->step['script'];
}

/**
* @return array|string[]
*/
public function getAfterScript()
{
if (isset($this->step['after-script'])) {
return $this->step['after-script'];
}

return array();
}

/**
* @return bool
*/
Expand Down Expand Up @@ -196,15 +209,32 @@ private function parseScript(array $step)
if (!isset($step['script'])) {
ParseException::__("'step' requires a script");
}
if (!is_array($step['script']) || !count($step['script'])) {
ParseException::__("'script' requires a list of commands");
$this->parseNamedScript('script', $step);
}

private function parseAfterScript(array $step)
{
if (isset($step['after-script'])) {
$this->parseNamedScript('after-script', $step);
}
}

/**
* @param $script
* @param mixed $name
*/
private function parseNamedScript($name, array $script)
{
if (!is_array($script[$name]) || !count($script[$name])) {
ParseException::__("'${name}' requires a list of commands");
}

foreach ($step['script'] as $index => $line) {
foreach ($script[$name] as $index => $line) {
if (!is_scalar($line) && null !== $line) {
ParseException::__(
sprintf(
"'script' requires a list of commands, step #%d is not a command",
"'%s' requires a list of commands, step #%d is not a command",
$name,
$index
)
);
Expand Down
50 changes: 39 additions & 11 deletions src/Runner/StepRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -470,26 +470,36 @@ private function pipHostConfigBind($mountPoint)
*/
private function runStepScript(Step $step, Streams $streams, Exec $exec, $name)
{
$script = $step->getScript();
$cmdBuffer = Lib::cmd("<<'SCRIPT' docker", array(
'exec', '-i', $name, '/bin/sh',
)) . "\n";

$buffer = Lib::cmd("<<'SCRIPT' docker", array(
'exec', '-i', $name, '/bin/sh',
));
$buffer .= "\n# this /bin/sh script is generated from a pipelines pipeline:\n";
$buffer = $cmdBuffer;
$buffer .= "# this /bin/sh script is generated from a pipeline step.\n";
$buffer .= "set -e\n";
foreach ($script as $line => $command) {
$line && $buffer .= 'printf \'\\n\'' . "\n";
$buffer .= 'printf \'\\035+ %s\\n\' ' . Lib::quoteArg($command) . "\n";
$buffer .= $command . "\n";
}
$buffer .= "SCRIPT\n";
$buffer .= $this->generateScript($step->getScript());

$status = $exec->pass($buffer, array());

if (0 !== $status) {
$streams->err(sprintf("script non-zero exit status: %d\n", $status));
}

if (!($script = $step->getAfterScript())) {
return $status;
}

$streams->out("After script:\n");
$buffer = $cmdBuffer;
$buffer .= "# pipelines generated after-script.\n";
$buffer .= sprintf("BITBUCKET_EXIT_CODE=%d\n", $status);
$buffer .= "export BITBUCKET_EXIT_CODE\n";
$buffer .= $this->generateScript($script);
$afterStatus = $exec->pass($buffer, array());
if (0 !== $afterStatus) {
$streams->err(sprintf("after-script non-zero exit status: %d\n", $afterStatus));
}

return $status;
}

Expand Down Expand Up @@ -522,4 +532,22 @@ private function shutdownStepContainer(StepContainer $container, $status)
));
}
}

/**
* @param array|string[] $script
*
* @return string
*/
private function generateScript(array $script)
{
$buffer = '';
foreach ($script as $line => $command) {
$line && $buffer .= 'printf \'\\n\'' . "\n";
$buffer .= 'printf \'\\035+ %s\\n\' ' . Lib::quoteArg($command) . "\n";
$buffer .= $command . "\n";
}
$buffer .= "SCRIPT\n";

return $buffer;
}
}
19 changes: 19 additions & 0 deletions tests/data/yml/after-script.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
clone:
depth: 1
image: ktomk/pipelines:busybox

pipelines:
default:
- step:
name: Happy
script:
- echo "fine"
after-script:
- 'echo "BITBUCKET_EXIT_CODE: ${BITBUCKET_EXIT_CODE}"'
- step:
name: Broken
script:
- exit 123
- echo "already broken"
after-script:
- 'echo "BITBUCKET_EXIT_CODE: ${BITBUCKET_EXIT_CODE}"'
10 changes: 10 additions & 0 deletions tests/unit/File/Pipeline/StepTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ public function testManual()
$this->assertTrue($this->createStep($manualStep, 1)->isManual(), 'second step can be manual');
}

public function testGetAfterScript()
{
$afterScriptStep = array(
'script' => array(':'),
'after-script' => array(':'),
);
$this->assertSame(array(), $this->createStep(null)->getAfterScript());
$this->assertSame(array(':'), $this->createStep($afterScriptStep)->getAfterScript());
}

/**
* @param null|array $array [optional]
*
Expand Down
24 changes: 23 additions & 1 deletion tests/unit/Runner/RunnerTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
namespace Ktomk\Pipelines\Runner;

use Ktomk\Pipelines\DestructibleString;
use Ktomk\Pipelines\File\File;
use Ktomk\Pipelines\File\Pipeline;
use Ktomk\Pipelines\File\Pipeline\Step;
use Ktomk\Pipelines\LibTmp;
use Ktomk\Pipelines\TestCase;
use Ktomk\Pipelines\Yaml\Yaml;
use PHPUnit\Framework\MockObject\MockObject;

/**
Expand Down Expand Up @@ -51,7 +53,7 @@ protected function setUp()
/**
* @param null|array $extra
*
* @return \Ktomk\Pipelines\File\Pipeline\Step
* @return Step
*/
protected function createTestStep(array $extra = null)
{
Expand All @@ -73,6 +75,26 @@ protected function createTestStep(array $extra = null)
return $step;
}

/**
* @param string $file
* @param int $step
* @param string $pipeline
*
* @return Step
*/
protected function createTestStepFromFixture($file, $step = 1, $pipeline = 'default')
{
$path = __DIR__ . '/../../data/yml/' . $file;
$array = Yaml::file($path);
$fileObject = new File($array);
$pipelineObject = $fileObject->getById($pipeline);
$this->assertNotNull($pipelineObject);
$pipelineSteps = $pipelineObject->getSteps();
$this->assertArrayHasKey($step - 1, $pipelineSteps);

return $pipelineSteps[$step - 1];
}

/**
* @param string $dir
*/
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/Runner/StepRunnerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,46 @@ public function testGetDockerBinaryRepository()
$this->assertInstanceOf('Ktomk\Pipelines\Runner\Docker\Binary\Repository', $actual);
}

public function testAfterScript()
{
$exec = new ExecTester($this);
$exec
->expect('capture', 'docker', 1, 'zap')
->expect('capture', 'docker', 0)
->expect('pass', '~<<\'SCRIPT\' docker exec ~', 0, 'script')
->expect('pass', '~<<\'SCRIPT\' docker exec ~', 0, 'after-script')
->expect('capture', 'docker', 0, 'docker kill')
->expect('capture', 'docker', 0, 'docker rm');

$step = $this->createTestStepFromFixture('after-script.yml');
$runner = $this->createTestStepRunner($exec, Flags::FLAGS, 'php://output');

$this->expectOutputRegex('{^After script:}m');

$status = $runner->runStep($step);
$this->assertSame(0, $status);
}

public function testAfterScriptFailing()
{
$exec = new ExecTester($this);
$exec
->expect('capture', 'docker', 1, 'zap')
->expect('capture', 'docker', 0)
->expect('pass', '~<<\'SCRIPT\' docker exec ~', 0, 'script')
->expect('pass', '~<<\'SCRIPT\' docker exec ~', 123, 'after-script')
->expect('capture', 'docker', 0, 'docker kill')
->expect('capture', 'docker', 0, 'docker rm');

$step = $this->createTestStepFromFixture('after-script.yml');
$runner = $this->createTestStepRunner($exec, Flags::FLAGS, array('php://output', 'php://output'));

$this->expectOutputRegex('{^after-script non-zero exit status: 123$}m');

$status = $runner->runStep($step);
$this->assertSame(0, $status);
}

private function keepContainerOnErrorExecTest(ExecTester $exec, $id = '*dry-run*')
{
$expectedRegex = sprintf(
Expand Down

0 comments on commit d1dea9b

Please sign in to comment.