diff --git a/CHANGELOG.md b/CHANGELOG.md index f5d553a..10c3178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project (post v2.1.0) adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Add `detect-rtl-changes` action to detect whether any RTL source reachable from a given top-level module changed. ## 2.5.1 - 2026-06-09 ### Added diff --git a/detect-rtl-changes/README.md b/detect-rtl-changes/README.md new file mode 100644 index 0000000..48d7ee2 --- /dev/null +++ b/detect-rtl-changes/README.md @@ -0,0 +1,87 @@ +# Detect RTL changes + +This action checks whether the RTL sources a given top-level RTL module depends on have changed between two commits. + +Internally, it uses `bender pickle --top ` to resolve the entire flat RTL a module depends on (including header files). +That output is hashed to produce a fingerprint of everything the module depends on. +Files not part of the Bender source graph (e.g. the scripts that drive compilation/simulation themselves) can additionally be watched. + +This directory ships two things: +- `detect-rtl-changes.sh`: a CI-agnostic script that can be used in Gitlab CIs; +- `action.yml`: a GitHub Actions composite action wrapping the script. + +## GitHub Actions usage + +### Step-level skip + +Gate a later step in the same job on the action's `rtl-changed` output: + +```yaml +jobs: + test: + runs-on: ubuntu-latest + steps: + - id: detect-rtl-changes + uses: pulp-platform/pulp-actions/detect-rtl-changes@v2.6.0 # update version as needed, not autoupdated + with: + top: tb_my_module + targets: test rtl + - if: steps.detect-rtl-changes.outputs.rtl-changed == 'true' + run: ./run_my_test.sh +``` + +### Job-level skip + +Gate a whole job based on the action's `rtl-changed` output, computed in a previous job: + +```yaml +jobs: + detect-rtl-changes: + runs-on: ubuntu-latest + outputs: + rtl-changed: ${{ steps.detect-rtl-changes.outputs.rtl-changed }} + steps: + - id: detect-rtl-changes + uses: pulp-platform/pulp-actions/detect-rtl-changes@v2.6.0 # update version as needed, not autoupdated + with: + top: tb_my_module + targets: test rtl + + test: + needs: detect-rtl-changes + if: needs.detect-rtl-changes.outputs.rtl-changed == 'true' + runs-on: ubuntu-latest + steps: + - run: ./run_my_test.sh +``` + +### Inputs + +| Input | Description | Default | +| --- | --- | --- | +| `top` | Space-separated top-level module(s) (`bender pickle --top`). Omit to hash all sources resolved by Bender. | `''` | +| `targets` | Space-separated Bender target(s) (`bender pickle -t`). | `''` | +| `watch-paths` | Space-separated extra path(s) checked via plain `git diff`. | `''` | +| `ref` | Git ref to diff. Set this to compare two arbitrary commits instead of the working tree against `compare-ref`. | current working tree | +| `compare-ref` | Git ref to diff against. | PR base commit / previous push commit / `HEAD^` | +| `bender-version` | Bender version to install. | latest | + +## GitLab CI usage + +GitLab CI can't consume a GitHub composite action directly, but a job can be gated by pulling and using the `detect-rtl-changes` script directly: + +```yaml +before_script: + - curl --proto '=https' --tlsv1.2 -sSf + https://raw.githubusercontent.com/pulp-platform/pulp-actions/v2.6.0/detect-rtl-changes/detect-rtl-changes.sh + -o ./detect-rtl-changes.sh + - chmod +x ./detect-rtl-changes.sh + +test: + script: + - ./detect-rtl-changes.sh -t test -t rtl -w .gitlab-ci.yml -- tb_my_module || exit 0 + - ./run_my_test.sh +``` + +By default, the current working tree (including any uncommitted changes) is diffed against `origin/$CI_DEFAULT_BRANCH` (or `origin/master` if `$CI_DEFAULT_BRANCH` is undefined). +Pass `-r/--ref` and `-c/--compare-ref` to diff two arbitrary commits. diff --git a/detect-rtl-changes/action.yml b/detect-rtl-changes/action.yml new file mode 100644 index 0000000..2c8aff3 --- /dev/null +++ b/detect-rtl-changes/action.yml @@ -0,0 +1,86 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +# Author: Luca Colagrande + +name: 'Detect RTL changes' +description: 'Check if the sources reachable from a top-level module changed, via `bender pickle` + content hashing.' + +inputs: + top: + description: 'Space-separated top-level module(s) to trim the source graph to (forwarded to `bender pickle --top`). + If omitted, the full (untrimmed) pickle is hashed, i.e. "did anything reachable by Bender change".' + required: false + default: '' + targets: + description: 'Space-separated Bender target(s) to include (forwarded to `bender pickle -t`, repeated per target).' + required: false + default: '' + watch-paths: + description: 'Space-separated extra path(s) to check for plain (non-Bender-tracked) changes via `git diff`, + e.g. the scripts that drive compilation/simulation themselves.' + required: false + default: '' + ref: + description: 'Git ref to diff. Defaults to the current working tree (including any uncommitted changes). + Set this to compare two arbitrary commits instead.' + required: false + default: '' + compare-ref: + description: 'Git ref to diff against. Defaults to the pull request base commit for pull_request events, the + previous commit for push events, or HEAD^ otherwise.' + required: false + default: '' + bender-version: + description: 'Bender version to install (default: latest)' + required: false + default: '' + +outputs: + rtl-changed: + description: '"true" if sources reachable from `top` changed with respect to `compare-ref`, "false" otherwise.' + value: ${{ steps.check.outputs.rtl-changed }} + +runs: + using: "composite" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install bender + uses: pulp-platform/pulp-actions/bender-install@v2.5.1 + with: + version: ${{ inputs.bender-version }} + - name: Resolve default compare ref + id: compare-ref + shell: bash + run: | + ref="${{ inputs.compare-ref }}" + if [ -z "$ref" ]; then + if [ "${{ github.event_name }}" = "pull_request" ]; then + ref="${{ github.event.pull_request.base.sha }}" + elif [ "${{ github.event_name }}" = "push" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then + ref="${{ github.event.before }}" + else + ref="HEAD^" + fi + fi + echo "ref=$ref" >> "$GITHUB_OUTPUT" + - name: Check if sources changed + id: check + shell: bash + run: | + set -f # Disable globbing: `top`/`targets`/`watch-paths` are plain space-separated lists, not globs. + args=(--compare-ref "${{ steps.compare-ref.outputs.ref }}") + if [ -n "${{ inputs.ref }}" ]; then args+=(--ref "${{ inputs.ref }}"); fi + for t in ${{ inputs.targets }}; do args+=(--target "$t"); done + for w in ${{ inputs.watch-paths }}; do args+=(--watch "$w"); done + args+=(--) + for m in ${{ inputs.top }}; do args+=("$m"); done + if "${{ github.action_path }}/detect-rtl-changes.sh" "${args[@]}"; then + echo "rtl-changed=true" >> "$GITHUB_OUTPUT" + else + echo "rtl-changed=false" >> "$GITHUB_OUTPUT" + fi diff --git a/detect-rtl-changes/detect-rtl-changes.sh b/detect-rtl-changes/detect-rtl-changes.sh new file mode 100755 index 0000000..f364cef --- /dev/null +++ b/detect-rtl-changes/detect-rtl-changes.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# +# Copyright 2026 ETH Zurich and University of Bologna. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 +# +# Author: Luca Colagrande +# +# Check whether the sources a CI job actually depends on changed. +# +# Background +# ---------- +# Hand-maintained `changes:`/`paths:` lists (one per CI job, listing every +# source file a testbench/module transitively depends on) are error-prone: +# it is easy to forget a dependency, and the list silently goes stale as the +# RTL evolves. See https://github.com/pulp-platform/axi/issues/432. +# +# This script replaces those lists with a content hash of the *actual* +# dependency closure, computed by Bender + slang instead of by hand: +# +# bender pickle --top [-t ]... +# +# statically elaborates the design from the given top-level module(s) and +# emits only the files reachable from it (trimming away unrelated sources), +# with header/include files inlined via `export_include_dirs`. Hashing that +# output gives a fingerprint of exactly what the job's outcome can depend on. +# The fingerprint is compared between two git refs; if they match, the job's +# real work can be skipped. Either side can be an arbitrary commit-ish (a +# branch, tag, or SHA); the "head" side defaults to the current working tree +# (including any uncommitted changes) if not given explicitly, which is the +# common case in CI. +# +# Any file that is not part of the Bender source graph (e.g. the scripts that +# drive compilation/simulation themselves) can additionally be watched with +# `-w/--watch`, which falls back to a plain `git diff` check. +# +# Usage +# ----- +# detect-rtl-changes.sh [-c REF] [-r REF] [-t TARGET]... [-w PATH]... [-- TOP...] +# +# -c, --compare-ref REF Git ref to diff against. +# (default: origin/$CI_DEFAULT_BRANCH, or origin/master) +# -r, --ref REF Git ref to diff. (default: the current working tree, +# including uncommitted changes) +# -t, --target TARGET Bender target to include (repeatable). Forwarded +# verbatim to `bender pickle -t TARGET`. +# -w, --watch PATH Extra path to check for plain (non-Bender-tracked) +# changes, e.g. a build/simulation script (repeatable). +# TOP... Top-level module(s) to trim the source graph to. +# If omitted, the full (untrimmed) pickle is hashed, +# i.e. "did anything reachable by Bender change". +# +# Exit status +# ----------- +# 0 the job should run (sources changed, or the check could not be +# performed conclusively -- this script fails open, never silently +# hiding a real change) +# 1 confirmed unchanged: the job can be safely skipped +# +# Typical use in a GitLab CI job's `script:`: +# +# detect-rtl-changes.sh -t test -t rtl -- tb_$TEST_MODULE || exit 0 +# +# For GitHub Actions, use the `detect-rtl-changes` action in this repository, +# which wraps this script and exposes an `rtl-changed` (true/false) step +# output instead of an exit code, since GitHub Actions can only condition +# steps and jobs on string outputs, not on another step's exit code. +# +# Reusability +# ----------- +# This script has no repository-specific logic; it only assumes a +# Bender-managed package. It can be dropped into (or curled by) any +# Bender-based repository as-is. + +set -uo pipefail + +compare_ref="origin/${CI_DEFAULT_BRANCH:-master}" +head_ref="" +targets=() +watch_paths=() + +while [ $# -gt 0 ]; do + case "$1" in + -c|--compare-ref) + compare_ref="$2"; shift 2 ;; + -r|--ref) + head_ref="$2"; shift 2 ;; + -t|--target) + targets+=("$2"); shift 2 ;; + -w|--watch) + watch_paths+=("$2"); shift 2 ;; + --) + shift; break ;; + -h|--help) + sed -n '2,68p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -*) + echo "detect-rtl-changes: unknown option '$1', running job to be safe" >&2 + exit 0 ;; + *) + break ;; + esac +done +tops=("$@") + +target_args=() +for t in "${targets[@]}"; do + target_args+=(-t "$t") +done + +top_args=() +if [ ${#tops[@]} -gt 0 ]; then + top_args=(--top "${tops[@]}") +fi + +run_because() { + echo "detect-rtl-changes: $1, running job" >&2 + exit 0 +} + +command -v bender >/dev/null 2>&1 || run_because "bender not found" +git rev-parse --is-inside-work-tree >/dev/null 2>&1 || run_because "not a git repository" + +# Resolve a ref to a commit SHA locally, fetching it if necessary (CI +# checkouts are often shallow and may not have it yet). +resolve_ref() { + local ref="$1" + if ! git rev-parse --verify --quiet "$ref" >/dev/null; then + local remote="${ref%%/*}" + local branch="${ref#*/}" + git fetch --quiet --depth=1 "$remote" "$branch" 2>/dev/null || true + fi + git rev-parse --verify --quiet "$ref" +} + +if ! base_sha=$(resolve_ref "$compare_ref"); then + run_because "could not resolve compare ref '$compare_ref'" +fi + +head_sha="" +if [ -n "$head_ref" ]; then + if ! head_sha=$(resolve_ref "$head_ref"); then + run_because "could not resolve ref '$head_ref'" + fi +fi + +# Plain-diff watch paths (files outside the Bender source graph). With no +# explicit head ref, this compares the base ref against the working tree +# (including uncommitted changes); otherwise it compares the two refs. +if [ ${#watch_paths[@]} -gt 0 ]; then + if ! git diff --quiet "$base_sha" ${head_sha:+"$head_sha"} -- "${watch_paths[@]}" 2>/dev/null; then + run_because "watched path(s) changed (${watch_paths[*]})" + fi +fi + +pickle_hash() { + bender -d "$1" pickle "${target_args[@]}" "${top_args[@]}" --no-progress 2>/dev/null \ + | sha256sum | cut -d' ' -f1 +} + +worktrees=() +cleanup() { + for w in "${worktrees[@]}"; do + git worktree remove --force "$w" >/dev/null 2>&1 + rm -rf "$w" + done +} +trap cleanup EXIT + +# Check out a ref into a fresh worktree, reusing already-cloned dependencies +# instead of re-fetching them, and print the worktree's path. +checkout_ref() { + local ref="$1" sha="$2" worktree + worktree=$(mktemp -d) + worktrees+=("$worktree") + git worktree add --detach --quiet "$worktree" "$sha" >/dev/null 2>&1 || return 1 + [ -d .bender ] && ln -s "$(pwd)/.bender" "$worktree/.bender" + echo "$worktree" +} + +if [ -n "$head_ref" ]; then + if ! head_dir=$(checkout_ref "$head_ref" "$head_sha"); then + run_because "could not check out '$head_ref' into a worktree" + fi +else + head_dir="." +fi +head_hash=$(pickle_hash "$head_dir") +[ -n "$head_hash" ] || run_because "failed to pickle sources at '${head_ref:-the current working tree}'" + +if ! base_dir=$(checkout_ref "$compare_ref" "$base_sha"); then + run_because "could not check out '$compare_ref' into a worktree" +fi +base_hash=$(pickle_hash "$base_dir") +[ -n "$base_hash" ] || run_because "failed to pickle sources at '$compare_ref'" + +if [ "$head_hash" = "$base_hash" ]; then + echo "detect-rtl-changes: no sources reachable from '${tops[*]:-}' changed between ${head_ref:-the current working tree} and $compare_ref, skipping" >&2 + exit 1 +fi + +echo "detect-rtl-changes: sources reachable from '${tops[*]:-}' changed between ${head_ref:-the current working tree} and $compare_ref, running job" >&2 +exit 0