From 90a92f9280abefaa05c3a4edcc94039a837c4793 Mon Sep 17 00:00:00 2001 From: Christopher Fitzner Date: Thu, 26 Mar 2026 17:11:22 -0700 Subject: [PATCH] Add get-workflow-ref action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composite action that resolves the ref a caller used to invoke a reusable workflow from this repo. Parses the caller's workflow file — no API calls or extra permissions needed. - Checks out caller workflows into a temp directory to avoid clobbering workspace state. - Uses two-stage grep (uses: lines + fixed-string match) to avoid false positives from comments, job names, or regex dot matching. - Strips @-suffix independently of github.ref so it works on pull_request events where github.ref differs from workflow_ref. - Includes 10 tests covering tag/branch/SHA refs, local ./ refs, PR event ref mismatch, regex false positives, and error cases. Co-Authored-By: roachdev-claude --- CHANGELOG.md | 3 + get-workflow-ref/action.yml | 42 ++++++ get-workflow-ref/resolve_ref.sh | 51 +++++++ get-workflow-ref/resolve_ref_test.sh | 209 +++++++++++++++++++++++++++ 4 files changed, 305 insertions(+) create mode 100644 get-workflow-ref/action.yml create mode 100755 get-workflow-ref/resolve_ref.sh create mode 100644 get-workflow-ref/resolve_ref_test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5a547..e6fe457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Breaking changes are prefixed with "Breaking Change: ". ### Added +- `get-workflow-ref` action: resolve the ref a caller used to invoke a reusable + workflow by parsing the caller's workflow file — no API calls or extra + permissions needed. - Shared shell helpers (`actions_helpers.sh`) and test framework (`test_helpers.sh`) for consistent logging, output handling, and test assertions across actions. diff --git a/get-workflow-ref/action.yml b/get-workflow-ref/action.yml new file mode 100644 index 0000000..2d150ca --- /dev/null +++ b/get-workflow-ref/action.yml @@ -0,0 +1,42 @@ +name: Get Workflow Ref +description: > + Resolve the ref that a caller used to invoke a reusable workflow from this + repo. Parses the caller's workflow file — no API calls or extra permissions + needed. + +inputs: + workflow_name: + description: > + Substring to match in the caller's uses line (e.g. + "cockroachdb/actions/.github/workflows/github-issue-autosolve.yml"). + required: true + +outputs: + ref: + description: The ref (tag, branch, or SHA) from the caller's uses line. + value: ${{ steps.resolve.outputs.ref }} + +runs: + using: "composite" + steps: + - name: Checkout caller workflows + uses: actions/checkout@v6 + with: + sparse-checkout: .github/workflows + persist-credentials: false + path: __get-workflow-ref + + - name: Resolve ref from caller workflow + id: resolve + shell: bash + working-directory: __get-workflow-ref + run: bash "$GITHUB_ACTION_PATH/resolve_ref.sh" + env: + WORKFLOW_REF: ${{ github.workflow_ref }} + REPO: ${{ github.repository }} + REF: ${{ github.ref }} + WORKFLOW_NAME: ${{ inputs.workflow_name }} + + - name: Cleanup + shell: bash + run: rm -rf __get-workflow-ref diff --git a/get-workflow-ref/resolve_ref.sh b/get-workflow-ref/resolve_ref.sh new file mode 100755 index 0000000..3ee01cb --- /dev/null +++ b/get-workflow-ref/resolve_ref.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Resolve the ref a caller used to invoke a reusable workflow. +# +# Required env vars: +# WORKFLOW_REF — github.workflow_ref (e.g. owner/repo/.github/workflows/caller.yml@refs/heads/main) +# REPO — github.repository (e.g. owner/repo) +# REF — github.ref (e.g. refs/heads/main) +# WORKFLOW_NAME — substring to match in the caller's uses line +# +# Outputs (appended to $GITHUB_OUTPUT): +# ref — the resolved ref string +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" +# shellcheck source=../actions_helpers.sh +source "$SCRIPT_DIR/../actions_helpers.sh" + +workflow_ref="${WORKFLOW_REF}" + +# Strip "owner/repo/" prefix and "@refs/..." suffix to get the +# caller's workflow file path. +workflow_path="${workflow_ref#"${REPO}/"}" +workflow_path="${workflow_path%@*}" + +if [ ! -f "$workflow_path" ]; then + log_error "Could not find caller workflow at: $workflow_path" + exit 1 +fi + +# Find the uses: line that references our workflow and extract the ref. +# Filter to uses: lines first (avoids matching comments or job names), +# then match the workflow name as a literal string. +if ! match="$(grep 'uses:' "$workflow_path" | grep --fixed-strings -- "$WORKFLOW_NAME" | head -1)"; then + log_error "Could not find '$WORKFLOW_NAME' in $workflow_path" + exit 1 +fi + +# Handle local reference (./): use the caller's own ref. +if [[ "$match" == *"./.github/workflows"* ]]; then + ref="${REF}" +else + ref="$(printf '%s\n' "$match" | sed 's/.*@//' | awk '{print $1}')" +fi + +if [ -z "$ref" ]; then + log_error "Could not parse ref from: $match" + exit 1 +fi + +set_output "ref" "$ref" +log_notice "Resolved workflow ref: $ref" diff --git a/get-workflow-ref/resolve_ref_test.sh b/get-workflow-ref/resolve_ref_test.sh new file mode 100644 index 0000000..7041e60 --- /dev/null +++ b/get-workflow-ref/resolve_ref_test.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# Tests for resolve_ref.sh +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +SCRIPT_DIR="$PWD" +source ../test_helpers.sh + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT +cd "$TMPDIR" + +export GITHUB_OUTPUT="$TMPDIR/github_output.txt" + +reset_output() { + : > "$GITHUB_OUTPUT" +} + +get_ref() { + grep "ref=" "$GITHUB_OUTPUT" | cut -d= -f2 +} + +# Helper: create a caller workflow file with the given uses line. +make_workflow() { + local path="$TMPDIR/$1" + mkdir -p "$(dirname "$path")" + cat > "$path" < "$path" <<'EOF' +name: Caller +on: workflow_dispatch +jobs: + call: + uses: cockroachdb/actions/.github/workflows/autosolvexyml@v1.0.0 +EOF + env WORKFLOW_REF="myorg/myrepo/.github/workflows/caller.yml@refs/heads/main" \ + REPO="myorg/myrepo" \ + REF="refs/heads/main" \ + WORKFLOW_NAME="autosolve.yml" \ + "$SCRIPT_DIR/resolve_ref.sh" +} +expect_failure "grep regex: dot in name should not match arbitrary char" test_grep_regex_false_positive + +# ============================================= +# Only match uses: lines, not comments or job names +# ============================================= + +reset_output +test_ignores_comments_and_job_names() { + local path="$TMPDIR/.github/workflows/caller.yml" + mkdir -p "$(dirname "$path")" + cat > "$path" <<'EOF' +name: Caller +on: workflow_dispatch +jobs: + # This job calls autosolve.yml to fix issues + autosolve.yml-runner: + uses: cockroachdb/actions/.github/workflows/autosolve.yml@v3.0.0 +EOF + env WORKFLOW_REF="myorg/myrepo/.github/workflows/caller.yml@refs/heads/main" \ + REPO="myorg/myrepo" \ + REF="refs/heads/main" \ + WORKFLOW_NAME="autosolve.yml" \ + "$SCRIPT_DIR/resolve_ref.sh" + [ "$(get_ref)" = "v3.0.0" ] +} +expect_success "uses-only: ignores comments and job names, matches uses: line" test_ignores_comments_and_job_names + +# ============================================= +# Error cases +# ============================================= + +reset_output +test_missing_workflow_file() { + env WORKFLOW_REF="myorg/myrepo/.github/workflows/nonexistent.yml@refs/heads/main" \ + REPO="myorg/myrepo" \ + REF="refs/heads/main" \ + WORKFLOW_NAME="autosolve.yml" \ + "$SCRIPT_DIR/resolve_ref.sh" +} +expect_failure "error: missing workflow file" "Could not find caller workflow" test_missing_workflow_file + +reset_output +test_workflow_name_not_found() { + make_workflow ".github/workflows/caller.yml" \ + "cockroachdb/actions/.github/workflows/other.yml@v1.0.0" + env WORKFLOW_REF="myorg/myrepo/.github/workflows/caller.yml@refs/heads/main" \ + REPO="myorg/myrepo" \ + REF="refs/heads/main" \ + WORKFLOW_NAME="nonexistent-workflow.yml" \ + "$SCRIPT_DIR/resolve_ref.sh" +} +expect_failure "error: workflow name not found" "Could not find 'nonexistent-workflow.yml'" test_workflow_name_not_found + +print_results