Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide correct SHA for GitHub actions (#67) #69

Merged
merged 14 commits into from
Mar 4, 2022
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Fixed
- Provide correct SHA for GitHub Actions ([#67](https://github.com/cucumber/ci-environment/issues/67), [#69](https://github.com/cucumber/ci-environment/pull/69))

## [9.0.0] - 2022-01-25
### Added
- [JavaScript] export `Env`, `Git`, `CiEnvironment` and `CiEnvironments` types
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package io.cucumber.cienvironment;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static io.cucumber.cienvironment.VariableExpression.evaluate;
import static java.util.Objects.requireNonNull;
Expand All @@ -11,6 +17,7 @@
import static java.util.Optional.ofNullable;

final class CiEnvironmentImpl implements CiEnvironment {
public static final Pattern GITHUB_PULL_REQUEST_ACTION_BEFORE = Pattern.compile(".*\"before\"\\s*:\\s*\"([a-f0-9]+)\".*");
public String name;
public String url;
public String buildNumber;
Expand Down Expand Up @@ -46,33 +53,51 @@ public Optional<CiEnvironment.Git> getGit() {
return ofNullable(git);
}

Optional<CiEnvironment> detect(Map<String, String> env) {
Optional<CiEnvironment> detect(Map<String, String> env, Function<Path, Stream<String>> getLines) {
String url = evaluate(getUrl(), env);
if (url == null) return empty();

return of(new CiEnvironmentImpl(
name,
url,
evaluate(getBuildNumber().orElse(null), env),
detectGit(env)
detectGit(env, getLines)
));
}

private Git detectGit(Map<String, String> env) {
String revision = evaluate(git.revision, env);
private Git detectGit(Map<String, String> env, Function<Path, Stream<String>> getLines) {
String revision = evaluateRevision(env, getLines);
if (revision == null) return null;

String remote = evaluate(git.remote, env);
if (remote == null) return null;

return new Git(
RemoveUserInfo.fromUrl(remote),
revision,
evaluate(git.branch, env),
evaluate(git.tag, env)
RemoveUserInfo.fromUrl(remote),
revision,
evaluate(git.branch, env),
evaluate(git.tag, env)
);
}

private String evaluateRevision(Map<String, String> env, Function<Path, Stream<String>> getLines) {
if ("pull_request".equals(env.get("GITHUB_EVENT_NAME"))) {
if (env.get("GITHUB_EVENT_PATH") == null) {
throw new RuntimeException("GITHUB_EVENT_PATH not set");
}
Path path = Paths.get(env.get("GITHUB_EVENT_PATH"));
return getBeforeProperty(getLines.apply(path));
}
return evaluate(git.revision, env);
}

static String getBeforeProperty(Stream<String> lines) {
return lines.filter(line -> GITHUB_PULL_REQUEST_ACTION_BEFORE.matcher(line).matches()).findFirst().map(line -> {
Matcher matcher = GITHUB_PULL_REQUEST_ACTION_BEFORE.matcher(line);
return matcher.matches() ? matcher.group(1) : null;
}).orElse(null);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
package io.cucumber.cienvironment;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

public final class DetectCiEnvironment {
private DetectCiEnvironment() {

}

public static Optional<CiEnvironment> detectCiEnvironment(Map<String, String> env) {
return detectCiEnvironment(env, (path) -> {
try {
return Files.lines(path);
} catch (IOException e) {
throw new RuntimeException("Couldn't read " + path, e);
}
});
}

static Optional<CiEnvironment> detectCiEnvironment(Map<String, String> env, Function<Path, Stream<String>> getLines) {
return CiEnvironments.TEMPLATES.stream()
.map(ciEnvironment -> ciEnvironment.detect(env))
.map(ciEnvironment -> ciEnvironment.detect(env, getLines))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.cucumber.cienvironment;

import org.junit.jupiter.api.Test;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

class CiEnvironmentImplTest {
@Test
void reads_before_property_from_multiline_json() {
Stream<String> lines = Stream.of(
"{\n",
" \"after\": \"3738117e3337e54955580f4e98cf767d96b42135\",\n",
" \"before\": \"06aa815724888e86f37e41e43e07b0ec1bb0ffe1\",",
" \"action\": \"synchronize\",\n",
"}\n"
);
String rev = CiEnvironmentImpl.getBeforeProperty(lines);
assertEquals("06aa815724888e86f37e41e43e07b0ec1bb0ffe1", rev);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

import static io.cucumber.cienvironment.DetectCiEnvironment.detectCiEnvironment;
import static java.nio.file.Files.newBufferedReader;
Expand All @@ -41,7 +42,7 @@ private static List<Path> acceptance_tests_pass() throws IOException {
@ParameterizedTest
@MethodSource
void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation) {
Optional<CiEnvironment> ciEnvironment = detectCiEnvironment(expectation.env);
Optional<CiEnvironment> ciEnvironment = detectCiEnvironment(expectation.env, path -> Stream.of("{\"before\": \"2436f28fad432a895bfc595bce16e907144b0dc3\"}"));
assertEquals(expectation.getExpected(), ciEnvironment);
}

Expand Down
47 changes: 41 additions & 6 deletions javascript/src/detectCiEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { readFileSync } from 'fs'

import { CiEnvironments } from './CiEnvironments.js'
import evaluateVariableExpression from './evaluateVariableExpression.js'
import { CiEnvironment, Env, Git } from './types.js'

export default function detectCiEnvironment(env: Env): CiEnvironment | undefined {
export type SyncFileReader = (path: string) => Buffer

export type GithubActionsEvent = {
before: string
}

export default function detectCiEnvironment(
env: Env,
fileReader: SyncFileReader = readFileSync
): CiEnvironment | undefined {
for (const ciEnvironment of CiEnvironments) {
const detected = detect(ciEnvironment, env)
const detected = detect(ciEnvironment, env, fileReader)
if (detected) {
return detected
}
Expand All @@ -23,8 +34,12 @@ export function removeUserInfoFromUrl(value: string): string {
}
}

function detectGit(ciEnvironment: CiEnvironment, env: Env): Git | undefined {
const revision = evaluateVariableExpression(ciEnvironment.git?.revision, env)
function detectGit(
ciEnvironment: CiEnvironment,
env: Env,
syncFileReader: SyncFileReader
): Git | undefined {
const revision = detectRevision(ciEnvironment, env, syncFileReader)
if (!revision) {
return undefined
}
Expand All @@ -45,15 +60,35 @@ function detectGit(ciEnvironment: CiEnvironment, env: Env): Git | undefined {
}
}

function detect(ciEnvironment: CiEnvironment, env: Env): CiEnvironment | undefined {
function detectRevision(
ciEnvironment: CiEnvironment,
env: Env,
syncFileReader: SyncFileReader
): string | undefined {
if (env.GITHUB_EVENT_NAME === 'pull_request') {
if (!env.GITHUB_EVENT_PATH) throw new Error('GITHUB_EVENT_PATH not set')
const event = JSON.parse(syncFileReader(env.GITHUB_EVENT_PATH).toString())
if (!('before' in event)) {
throw new Error('No before property in ${env.GITHUB_EVENT_PATH}')
}
return event.before
}
return evaluateVariableExpression(ciEnvironment.git?.revision, env)
}

function detect(
ciEnvironment: CiEnvironment,
env: Env,
syncFileReader: SyncFileReader
): CiEnvironment | undefined {
const url = evaluateVariableExpression(ciEnvironment.url, env)
if (url === undefined) {
// The url is what consumers will use as the primary key for a build
// If this cannot be determined, we return nothing.
return undefined
}
const buildNumber = evaluateVariableExpression(ciEnvironment.buildNumber, env)
const git = detectGit(ciEnvironment, env)
const git = detectGit(ciEnvironment, env, syncFileReader)

return {
name: ciEnvironment.name,
Expand Down
10 changes: 9 additions & 1 deletion javascript/test/acceptanceTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from 'fs'
import glob from 'glob'
import path from 'path'

import { GithubActionsEvent, SyncFileReader } from '../src/detectCiEnvironment'
import detectCiEnvironment from '../src/index.js'
import { Env } from '../src/types.js'

Expand All @@ -12,7 +13,7 @@ describe('detectCiEnvironment', () => {
const envData = fs.readFileSync(txt, { encoding: 'utf8' })
const entries = envData.split(/\n/).map((line) => line.split(/=/))
const env: Env = Object.fromEntries(entries)
const ciEnvironment = detectCiEnvironment(env)
const ciEnvironment = detectCiEnvironment(env, gitHubActionReader)

const expectedJson = fs.readFileSync(`${txt}.json`, {
encoding: 'utf8',
Expand All @@ -21,3 +22,10 @@ describe('detectCiEnvironment', () => {
})
}
})

const gitHubActionReader: SyncFileReader = () => {
const event: GithubActionsEvent = {
before: '2436f28fad432a895bfc595bce16e907144b0dc3',
}
return Buffer.from(JSON.stringify(event, null, 2), 'utf-8')
}
27 changes: 19 additions & 8 deletions ruby/lib/cucumber/ci_environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ module CiEnvironment
extend VariableExpression
CI_ENVIRONMENTS_PATH = File.join(File.dirname(__FILE__), 'ci_environment/CiEnvironments.json')

def detect_ci_environment(env)
ci_environments = JSON.parse(IO.read(CI_ENVIRONMENTS_PATH))
def detect_ci_environment(env, file_reader = IO.method(:read))
ci_environments = JSON.parse(file_reader.call(CI_ENVIRONMENTS_PATH))
ci_environments.each do |ci_environment|
detected = detect(ci_environment, env)
detected = detect(ci_environment, env, file_reader)
return detected unless detected.nil?
end

nil
end

def detect(ci_environment, env)
def detect(ci_environment, env, file_reader)
url = evaluate(ci_environment['url'], env)
return nil if url.nil?

Expand All @@ -27,13 +27,13 @@ def detect(ci_environment, env)
buildNumber: evaluate(ci_environment['buildNumber'], env),
}

detected_git = detect_git(ci_environment, env)
detected_git = detect_git(ci_environment, env, file_reader)
result[:git] = detected_git if detected_git
result
end

def detect_git(ci_environment, env)
revision = evaluate(ci_environment['git']['revision'], env)
def detect_git(ci_environment, env, file_reader)
revision = detect_revision(ci_environment, env, file_reader)
return nil if revision.nil?

remote = evaluate(ci_environment['git']['remote'], env)
Expand All @@ -51,6 +51,17 @@ def detect_git(ci_environment, env)
git_info
end

def detect_revision(ci_environment, env, file_reader)
if env['GITHUB_EVENT_NAME'] == 'pull_request'
raise StandardError('GITHUB_EVENT_PATH not set') unless env['GITHUB_EVENT_PATH']
event = JSON.parse(file_reader.call(env['GITHUB_EVENT_PATH']))
raise StandardError('GITHUB_EVENT_PATH not set') unless event['before']
return event['before']
end

return evaluate(ci_environment['git']['revision'], env)
end

def remove_userinfo_from_url(value)
return nil if value.nil?

Expand All @@ -63,6 +74,6 @@ def remove_userinfo_from_url(value)
end
end

module_function :detect_ci_environment, :detect, :detect_git, :remove_userinfo_from_url
module_function :detect_ci_environment, :detect, :detect_git, :detect_revision, :remove_userinfo_from_url
end
end
7 changes: 6 additions & 1 deletion ruby/spec/cucumber/ci_environment/ci_environment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
context "with #{File.basename(test_data_file, '.txt')}" do
subject { JSON.parse(ci_environment.to_json) }

let(:ci_environment) { Cucumber::CiEnvironment.detect_ci_environment(env) }
def mock_reader(path)
return '{"before": "2436f28fad432a895bfc595bce16e907144b0dc3"}' if path.end_with?('_github_workflow/event.json')
IO.read(path)
end

let(:ci_environment) { Cucumber::CiEnvironment.detect_ci_environment(env, method(:mock_reader)) }
let(:env) { Hash[entries] }
let(:entries) { env_data.split(/\n/).map { |line| line.split(/=/) } }
let(:env_data) { IO.read(test_data_file) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ GITHUB_REPOSITORY=cucumber-ltd/shouty.rb
GITHUB_RUN_ID=154666429
GITHUB_SHA=99684bcacf01d95875834d87903dcb072306c9ad
GITHUB_REF=refs/heads/master
GITHUB_EVENT_NAME=push
7 changes: 7 additions & 0 deletions testdata/GitHubActionsPullRequest.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
GITHUB_SERVER_URL=https://github.com
GITHUB_REPOSITORY=cucumber-ltd/shouty.rb
GITHUB_RUN_ID=154666429
GITHUB_SHA=99684bcacf01d95875834d87903dcb072306c9ad
GITHUB_REF=refs/heads/master
GITHUB_EVENT_NAME=pull_request
GITHUB_EVENT_PATH=/home/runner/work/_temp/_github_workflow/event.json
10 changes: 10 additions & 0 deletions testdata/GitHubActionsPullRequest.txt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"git": {
"remote": "https://github.com/cucumber-ltd/shouty.rb.git",
"revision": "2436f28fad432a895bfc595bce16e907144b0dc3",
"branch": "master"
},
"name": "GitHub Actions",
"url": "https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429",
"buildNumber": "154666429"
}