Skip to content

Commit

Permalink
Pin 3rd party CI action versions
Browse files Browse the repository at this point in the history
GitHub allows to delete and re-publish a tag, so referencing 3rd party
action by tag name should be discouraged.
  • Loading branch information
findepi committed Jun 13, 2024
1 parent e9062c0 commit 2f39878
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 6 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/annotate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ jobs:
(cd "$name" && unzip ../"$archive")
done
# Process unit and integration test reports
- uses: scacap/[email protected]
- uses: scacap/action-surefire-report@7263a78ba060b395c8a0a0e58fc897efb1bddb7c # v1.6.2
with:
# this workflow should never fail, as it is not a quality gateway
fail_if_no_tests: false
commit: ${{github.event.workflow_run.head_sha}}
# Process Product Test reports
- uses: starburstdata/[email protected]
- uses: starburstdata/action-testng-report@ce8d903c91ed324a708db57174ec34416e8a3eea # v1.0.3
with:
# this workflow should never fail, as it is not a quality gateway
fail_if_empty: false
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ jobs:
$MAVEN failsafe:integration-test failsafe:verify -B --strict-checksums -P ci -pl :trino-jdbc
- name: Clean Maven Output
run: $MAVEN clean -pl '!:trino-server,!:trino-cli'
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3
with:
platforms: arm64,ppc64le
- name: Build and Test Docker Image
Expand Down Expand Up @@ -832,7 +832,7 @@ jobs:
with:
cache: restore
cleanup-node: true
- uses: dorny/paths-filter@v3
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
id: filter
with:
filters: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cleanup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
timeout-minutes: 5
steps:
- name: 'Cancel Runs For Closed PRs'
uses: styfle/[email protected]
uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1
with:
# Cancel workflow when PR closed. https://github.com/styfle/cancel-workflow-action#advanced-ignore-sha
ignore_sha: true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
docs: ${{ steps.filter.outputs.docs }}
other: ${{ steps.filter.outputs.other }}
steps:
- uses: dorny/paths-filter@v3
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
id: filter
with:
filters: |
Expand Down
202 changes: 202 additions & 0 deletions testing/trino-tests/src/test/java/io/trino/tests/ci/TestWorkflows.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.tests.ci;

import io.airlift.log.Logger;
import org.junit.jupiter.api.Test;
import org.yaml.snakeyaml.Yaml;

import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Verify.verifyNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static java.util.stream.Collectors.joining;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

public class TestWorkflows
{
private static final Logger log = Logger.get(TestWorkflows.class);

@Test
public void testWorkflows()
throws Exception
{
List<String> errors = new ArrayList<>();
listWorkflows().forEach(path -> errors.addAll(checkWorkflow(path)));
if (!errors.isEmpty()) {
fail("Errors: " + errors.stream()
.map("\n\t\t%s"::formatted)
.collect(joining()));
}
}

private static List<Path> listWorkflows()
throws Exception
{
try (Stream<Path> walk = Files.walk(findRepositoryRoot().resolve(".github/workflows"), 1)) {
return walk
.filter(path -> path.toString().endsWith(".yml"))
.collect(toImmutableList());
}
}

private static List<String> checkWorkflow(Path path)
{
List<String> errors = new ArrayList<>();
try {
Yaml yaml = new Yaml();
Map<?, ?> workflow = yaml.load(new StringReader(Files.readString(path)));
Map<String, ?> jobs = getMap(workflow, "jobs");
jobs.forEach((jobName, jobDefinition) -> {
Map<?, ?> job = (Map<?, ?>) jobDefinition;

List<?> steps = getList(job, "steps");
for (int stepPosition = 0; stepPosition < steps.size(); stepPosition++) {
Map<?, ?> step = (Map<?, ?>) steps.get(stepPosition);
String stepName = firstNonNull((String) step.get("name"), "Step #" + stepPosition);
if (step.containsKey("uses")) {
String uses = getString(step, "uses");
if (!isSafeActionReference(uses)) {
errors.add("Unsafe action reference in %s » %s » %s: %s. A reference to a 3rd party action is safe when it pins to a specific commit.".formatted(
path, jobName, stepName, uses));
}
}
}
});
}
catch (AssertionError | Exception e) {
throw new AssertionError("Failed when checking %s: %s".formatted(path, e), e);
}
return errors;
}

@Test
public void testActions()
throws Exception
{
List<String> errors = new ArrayList<>();
listActions().forEach(path -> errors.addAll(checkAction(path)));
if (!errors.isEmpty()) {
fail("Errors: " + errors.stream()
.map("\n\t\t%s"::formatted)
.collect(joining()));
}
}

private static List<Path> listActions()
throws Exception
{
Path actionsDir = findRepositoryRoot().resolve(".github/actions");
try (Stream<Path> walk = Files.walk(actionsDir, 1)) {
return walk
.filter(Predicate.not(actionsDir::equals))
.map(path -> path.resolve("action.yml"))
.collect(toImmutableList());
}
}

private static List<String> checkAction(Path path)
{
List<String> errors = new ArrayList<>();
try {
Yaml yaml = new Yaml();
Map<?, ?> workflow = yaml.load(new StringReader(Files.readString(path)));
Map<String, ?> runs = getMap(workflow, "runs");
assertThat(getString(runs, "using")).isEqualTo("composite"); // test only supports composite actions
List<?> steps = getList(runs, "steps");
for (int stepPosition = 0; stepPosition < steps.size(); stepPosition++) {
Map<?, ?> step = (Map<?, ?>) steps.get(stepPosition);
String stepName = firstNonNull((String) step.get("name"), "Step #" + stepPosition);
if (step.containsKey("uses")) {
String uses = getString(step, "uses");
if (!isSafeActionReference(uses)) {
errors.add("Unsafe action reference in %s » %s: %s. A reference to a 3rd party action is safe when it pins to a specific commit.".formatted(
path, stepName, uses));
}
}
}
}
catch (AssertionError | Exception e) {
throw new AssertionError("Failed when checking %s: %s".formatted(path, e), e);
}
return errors;
}

private static boolean isSafeActionReference(String actionName)
{
if (actionName.startsWith("actions/")) {
// standard action
return true;
}
if (actionName.startsWith("./")) {
// inline action
return true;
}
if (actionName.startsWith("trinodb/")) {
// our own
return true;
}
if (actionName.matches(".*@[0-9a-f]{40}$")) {
// Github disallows 40-character long hex-like strings as branch or tag names, so this must be a commit hash
// It's immutable and can't be changed by the owner of the repository.
return true;
}
return false;
}

private static Path findRepositoryRoot()
{
Path workingDirectory = Paths.get("").toAbsolutePath();
log.info("Current working directory: %s", workingDirectory);
for (Path path = workingDirectory; path != null; path = path.getParent()) {
if (Files.isDirectory(path.resolve(".git"))) {
return path;
}
}
throw new RuntimeException("Failed to find repository root from " + workingDirectory);
}

private static String getString(Map<?, ?> map, String key)
{
Object value = map.get(key);
verifyNotNull(value, "No or null entry for key [%s] in %s", key, map);
return (String) value;
}

private static List<?> getList(Map<?, ?> map, String key)
{
Object value = map.get(key);
verifyNotNull(value, "No or null entry for key [%s] in %s", key, map);
return (List<?>) value;
}

private static Map<String, ?> getMap(Map<?, ?> map, String key)
{
Object value = map.get(key);
verifyNotNull(value, "No or null entry for key [%s] in %s", key, map);
return ((Map<?, ?>) value).entrySet().stream()
.collect(toImmutableMap(e -> (String) e.getKey(), Map.Entry::getValue));
}
}

0 comments on commit 2f39878

Please sign in to comment.