diff --git a/.github/scripts/resolve-dependency-refs.sh b/.github/scripts/resolve-dependency-refs.sh new file mode 100644 index 000000000..f0adaebfe --- /dev/null +++ b/.github/scripts/resolve-dependency-refs.sh @@ -0,0 +1,382 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if (( $# < 3 || $# % 2 == 0 )); then + echo "Usage: $0 SOURCE_DIRECTORY OUTPUT_PREFIX DEPENDENCY_REPOSITORY [...]" >&2 + exit 2 +fi + +source_directory=$1 +shift + +source_branch=${SOURCE_BRANCH:?SOURCE_BRANCH must name the branch being tested} +integration_branch=${INTEGRATION_BRANCH:-staging} +root_branch=${ROOT_BRANCH:-master} + +declare -a dependency_prefixes=() +declare -a dependency_repositories=() +while (( $# )); do + dependency_prefixes+=("$1") + dependency_repositories+=("$2") + shift 2 +done + +repository_url() { + local repository=$1 + if [[ $repository == *://* || $repository == /* ]]; then + echo "$repository" + else + echo "https://github.com/${repository}.git" + fi +} + +evidence_directory=$(mktemp -d) +declare -a evidence_paths=("$source_directory") +declare -a evidence_ref_prefixes=("refs/remotes/origin/") +declare -A cloned_paths=() + +clone_evidence_repository() { + local repository=$1 + + if [[ -n ${cloned_paths[$repository]+yes} ]]; then + return + fi + + local clone_path="${evidence_directory}/repository-${#cloned_paths[@]}.git" + git clone --quiet --bare --filter=blob:none \ + "$(repository_url "$repository")" "$clone_path" + cloned_paths[$repository]=$clone_path + evidence_paths+=("$clone_path") + evidence_ref_prefixes+=("refs/heads/") +} + +for repository in "${dependency_repositories[@]}"; do + clone_evidence_repository "$repository" +done + +if [[ -n ${EVIDENCE_REPOSITORIES:-} ]]; then + read -r -a additional_evidence <<< "$EVIDENCE_REPOSITORIES" + for repository in "${additional_evidence[@]}"; do + clone_evidence_repository "$repository" + done +fi + +# A moved branch is relevant only when its name is shared by repositories. +# Count each repository once so private one-off branches do not pollute the +# global candidate set. +declare -A branch_presence=() +for ((repository_index = 0; repository_index < ${#evidence_paths[@]}; repository_index++)); do + repository_path=${evidence_paths[$repository_index]} + ref_prefix=${evidence_ref_prefixes[$repository_index]} + while IFS= read -r ref; do + branch=${ref#"$ref_prefix"} + [[ $branch == HEAD ]] && continue + branch_presence[$branch]=$(( ${branch_presence[$branch]:-0} + 1 )) + done < <( + git -C "$repository_path" for-each-ref \ + --format='%(refname)' "${ref_prefix%/}" + ) +done + +declare -a candidates=() +declare -A seen_candidates=() +declare -A candidate_proven=() + +add_candidate() { + local branch=$1 + local proven=${2:-true} + if [[ -n $branch && -z ${seen_candidates[$branch]+yes} ]]; then + seen_candidates[$branch]=${#candidates[@]} + candidate_proven[${#candidates[@]}]=$proven + candidates+=("$branch") + elif [[ -n $branch && $proven == true ]]; then + candidate_proven[${seen_candidates[$branch]}]=true + fi +} + +add_candidate "$source_branch" + +collect_candidates() { + local repository_path=$1 + local ref_prefix=$2 + local start_ref=$3 + local required=$4 + local root_ref="${ref_prefix}${root_branch}" + + if ! git -C "$repository_path" rev-parse --verify "${start_ref}^{commit}" >/dev/null 2>&1; then + if [[ $required == true ]]; then + echo "Cannot resolve the workflow commit in ${repository_path}" >&2 + exit 1 + fi + return + fi + if ! git -C "$repository_path" rev-parse --verify "${root_ref}^{commit}" >/dev/null 2>&1; then + echo "Cannot find root branch '${root_branch}' in ${repository_path}" >&2 + exit 1 + fi + + declare -A distance_by_commit=() + local distance=0 + local reached_root=false + local commit + while IFS= read -r commit; do + distance_by_commit[$commit]=$distance + ((distance += 1)) + if git -C "$repository_path" merge-base --is-ancestor "$commit" "$root_ref"; then + reached_root=true + break + fi + done < <(git -C "$repository_path" rev-list --first-parent "$start_ref") + + if [[ $reached_root != true ]]; then + if [[ $required == true ]]; then + echo "The workflow commit's first-parent history does not reach '${root_branch}'" >&2 + exit 1 + fi + return + fi + local root_distance=$((distance - 1)) + + local ordered_refs="${evidence_directory}/candidate-refs-${#candidates[@]}-${distance}" + : > "$ordered_refs" + local ref object branch + while IFS=$'\t' read -r ref object; do + branch=${ref#"$ref_prefix"} + [[ $branch == HEAD ]] && continue + [[ $branch == "$source_branch" ]] && continue + [[ $branch == "$integration_branch" ]] && continue + [[ $branch == "$root_branch" ]] && continue + + if [[ -n ${distance_by_commit[$object]+yes} ]]; then + printf '%s\t%s\ttrue\n' "${distance_by_commit[$object]}" "$branch" >> "$ordered_refs" + continue + fi + + # The branch may have advanced after a child was created. Its merge-base + # still identifies the old tip on the event's stack. Requiring the name in + # multiple repositories and a fork point newer than the root boundary + # avoids treating every branch from master as part of this stack. + [[ ${branch_presence[$branch]:-0} -ge 2 ]] || continue + merge_base=$(git -C "$repository_path" merge-base "$start_ref" "$ref" 2>/dev/null || true) + [[ -n $merge_base && -n ${distance_by_commit[$merge_base]+yes} ]] || continue + [[ ${distance_by_commit[$merge_base]} -lt $root_distance ]] || continue + printf '%s\t%s\tfalse\n' "${distance_by_commit[$merge_base]}" "$branch" >> "$ordered_refs" + done < <( + git -C "$repository_path" for-each-ref \ + --format='%(refname)%09%(objectname)' "${ref_prefix%/}" + ) + + while IFS=$'\t' read -r _ branch proven; do + add_candidate "$branch" "$proven" + done < <(sort -k1,1n -k2,2 -u "$ordered_refs") +} + +# HEAD is the immutable event commit. Never substitute the mutable remote tip. +collect_candidates "$source_directory" "refs/remotes/origin/" HEAD true + +for ((repository_index = 1; repository_index < ${#evidence_paths[@]}; repository_index++)); do + repository_path=${evidence_paths[$repository_index]} + source_ref="refs/heads/${source_branch}" + if git -C "$repository_path" show-ref --verify --quiet "$source_ref"; then + collect_candidates "$repository_path" "refs/heads/" "$source_ref" false + fi +done + +if [[ $source_branch != "$root_branch" ]]; then + add_candidate "$integration_branch" +fi +add_candidate "$root_branch" + +printf -v candidate_chain '%s -> ' "${candidates[@]}" +candidate_chain=${candidate_chain% -> } +echo "Dependency branch candidates: ${candidate_chain}" + +candidate_count=${#candidates[@]} +declare -A edges=() +declare -A strict_edges=() + +add_edge() { + local newer=$1 + local older=$2 + [[ $newer == "$older" ]] || edges["${newer},${older}"]=1 +} + +is_first_parent_ancestor() { + local repository_path=$1 + local older=$2 + local newer=$3 + local commit + + while IFS= read -r commit; do + [[ $commit == "$older" ]] && return 0 + done < <(git -C "$repository_path" rev-list --first-parent "$newer") + return 1 +} + +# The event branch is newest by definition. staging and master are the two +# conventional terminal levels, even when their tips have advanced. +for ((i = 0; i < candidate_count; i++)); do + branch=${candidates[$i]} + if [[ $branch != "$source_branch" ]]; then + add_edge 0 "$i" + fi + if [[ $source_branch != "$root_branch" && + $branch != "$integration_branch" && $branch != "$root_branch" ]]; then + integration_index=${seen_candidates[$integration_branch]} + add_edge "$i" "$integration_index" + fi +done +root_index=${seen_candidates[$root_branch]} +if [[ $source_branch != "$root_branch" ]]; then + integration_index=${seen_candidates[$integration_branch]} + add_edge "$integration_index" "$root_index" +fi + +# Each repository contributes only strict first-parent evidence. Equal refs +# are neutral; missing or diverged refs contribute no ordering. +for ((repository_index = 0; repository_index < ${#evidence_paths[@]}; repository_index++)); do + repository_path=${evidence_paths[$repository_index]} + ref_prefix=${evidence_ref_prefixes[$repository_index]} + + for ((i = 0; i < candidate_count; i++)); do + left_branch=${candidates[$i]} + [[ $left_branch == "$source_branch" || $left_branch == "$integration_branch" || $left_branch == "$root_branch" ]] && continue + left_ref="${ref_prefix}${left_branch}" + left_sha=$(git -C "$repository_path" rev-parse --verify "${left_ref}^{commit}" 2>/dev/null || true) + [[ -n $left_sha ]] || continue + + for ((j = i + 1; j < candidate_count; j++)); do + right_branch=${candidates[$j]} + [[ $right_branch == "$source_branch" || $right_branch == "$integration_branch" || $right_branch == "$root_branch" ]] && continue + right_ref="${ref_prefix}${right_branch}" + right_sha=$(git -C "$repository_path" rev-parse --verify "${right_ref}^{commit}" 2>/dev/null || true) + [[ -n $right_sha && $left_sha != "$right_sha" ]] || continue + + if is_first_parent_ancestor "$repository_path" "$right_sha" "$left_sha"; then + add_edge "$i" "$j" + strict_edges["${i},${j}"]=1 + elif is_first_parent_ancestor "$repository_path" "$left_sha" "$right_sha"; then + add_edge "$j" "$i" + strict_edges["${j},${i}"]=1 + fi + done + done +done + +# Strict evidence validates an uncertain candidate only when it connects to a +# branch already observed directly on a stack. Propagate that proof through a +# chain of strict first-parent relationships. +proof_changed=true +while [[ $proof_changed == true ]]; do + proof_changed=false + for edge in "${!strict_edges[@]}"; do + newer_index=${edge%,*} + older_index=${edge#*,} + if [[ ${candidate_proven[$newer_index]:-false} == true && + ${candidate_proven[$older_index]:-false} != true ]]; then + candidate_proven[$older_index]=true + proof_changed=true + fi + done +done + +# Compute transitive closure and reject contradictory evidence. +for ((k = 0; k < candidate_count; k++)); do + for ((i = 0; i < candidate_count; i++)); do + [[ -n ${edges["${i},${k}"]+yes} ]] || continue + for ((j = 0; j < candidate_count; j++)); do + if [[ -n ${edges["${k},${j}"]+yes} ]]; then + edges["${i},${j}"]=1 + fi + done + done +done + +for ((i = 0; i < candidate_count; i++)); do + if [[ -n ${edges["${i},${i}"]+yes} ]]; then + echo "Contradictory branch ordering evidence involves '${candidates[$i]}'" >&2 + exit 1 + fi +done + +for ((dependency_index = 0; dependency_index < ${#dependency_repositories[@]}; dependency_index++)); do + prefix=${dependency_prefixes[$dependency_index]} + repository=${dependency_repositories[$dependency_index]} + repository_path=${cloned_paths[$repository]} + + default_branch=$(git -C "$repository_path" symbolic-ref --short HEAD) + default_branch=${default_branch#refs/heads/} + + declare -a available_indices=() + declare -A available_shas=() + for ((i = 0; i < candidate_count; i++)); do + branch=${candidates[$i]} + sha=$(git -C "$repository_path" rev-parse --verify "refs/heads/${branch}^{commit}" 2>/dev/null || true) + if [[ -n $sha ]]; then + available_indices+=("$i") + available_shas[$i]=$sha + fi + done + + declare -a newest_indices=() + + # A merge-base-only branch may be a moved parent or an unrelated sibling. + # Without strict evidence elsewhere, selecting its distinct commit would be + # a guess. Equal target SHAs remain harmless aliases. + for i in "${available_indices[@]}"; do + [[ ${candidate_proven[$i]:-false} == true ]] && continue + harmless_uncertain=false + for j in "${available_indices[@]}"; do + if [[ ${candidate_proven[$j]:-false} == true && + ( ${available_shas[$j]} == "${available_shas[$i]}" || + -n ${edges["${j},${i}"]+yes} ) ]]; then + harmless_uncertain=true + break + fi + done + if [[ $harmless_uncertain != true ]]; then + echo "Cannot place shared branch '${candidates[$i]}' in the stack for ${repository}" >&2 + exit 1 + fi + done + + for i in "${available_indices[@]}"; do + dominated=false + for j in "${available_indices[@]}"; do + if [[ $i != "$j" && -n ${edges["${j},${i}"]+yes} ]]; then + dominated=true + break + fi + done + [[ $dominated == true ]] || newest_indices+=("$i") + done + + if (( ${#newest_indices[@]} == 0 )); then + echo "None of the candidate branches exists in ${repository}" >&2 + exit 1 + fi + + selected_index=${newest_indices[0]} + selected_sha=${available_shas[$selected_index]} + if (( ${#newest_indices[@]} > 1 )); then + ambiguous=() + for i in "${newest_indices[@]}"; do + ambiguous+=("${candidates[$i]}") + if [[ ${available_shas[$i]} != "$selected_sha" ]]; then + printf -v ambiguous_list '%s, ' "${ambiguous[@]}" + ambiguous_list=${ambiguous_list%, } + echo "Ambiguous newest branches in ${repository}: ${ambiguous_list}" >&2 + exit 1 + fi + done + fi + + selected_branch=${candidates[$selected_index]} + echo "Using '${selected_branch}' (${selected_sha}) for ${repository}" + { + echo "${prefix}_ref=${selected_sha}" + echo "${prefix}_branch=${selected_branch}" + echo "${prefix}_default=${default_branch}" + } >> "${GITHUB_OUTPUT:?GITHUB_OUTPUT is not set}" +done diff --git a/.github/scripts/test-resolve-dependency-refs.sh b/.github/scripts/test-resolve-dependency-refs.sh new file mode 100644 index 000000000..a235f1c8e --- /dev/null +++ b/.github/scripts/test-resolve-dependency-refs.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_directory=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +resolver=${1:-"${script_directory}/resolve-dependency-refs.sh"} +test_directory=$(mktemp -d) +source_repository="${test_directory}/source" + +git init --quiet --initial-branch=master "$source_repository" +git -C "$source_repository" config user.name "CI Resolver Test" +git -C "$source_repository" config user.email "ci-resolver@example.invalid" + +commit() { + local message=$1 + git -C "$source_repository" commit --quiet --allow-empty -m "$message" +} + +commit "master base" + +git -C "$source_repository" switch --quiet -c staging +commit "staging base" + +git -C "$source_repository" switch --quiet -c lmsousa +commit "branch A" +lmsousa_tip=$(git -C "$source_repository" rev-parse HEAD) + +git -C "$source_repository" switch --quiet -c vitest +commit "branch B" +vitest_tip=$(git -C "$source_repository" rev-parse HEAD) + +git -C "$source_repository" switch --quiet -c java-deprecation +commit "branch C" +java_deprecation_tip=$(git -C "$source_repository" rev-parse HEAD) + +git -C "$source_repository" switch --quiet -c workflow-fix +commit "current branch" +workflow_fix_tip=$(git -C "$source_repository" rev-parse HEAD) + +# Both long-lived branches advance after the stack was created. +git -C "$source_repository" switch --quiet staging +commit "advanced staging" +staging_tip=$(git -C "$source_repository" rev-parse HEAD) + +git -C "$source_repository" switch --quiet master +commit "advanced master" +master_tip=$(git -C "$source_repository" rev-parse HEAD) + +# Simulate a later push after the workflow event was created. Resolution must +# use the detached event commit, never the newer remote source branch tip. +git -C "$source_repository" switch --quiet workflow-fix +commit "post-event parent branch" +later_parent_tip=$(git -C "$source_repository" rev-parse HEAD) +git -C "$source_repository" branch later-parent +commit "post-event source update" +post_event_source_tip=$(git -C "$source_repository" rev-parse HEAD) +git -C "$source_repository" switch --quiet --detach "$workflow_fix_tip" + +git -C "$source_repository" switch --quiet lmsousa +commit "advanced parent branch" +advanced_lmsousa_tip=$(git -C "$source_repository" rev-parse HEAD) + +git -C "$source_repository" switch --quiet staging +git -C "$source_repository" switch --quiet -c rebased-parent-fixture +commit "rebased parent branch" +rebased_lmsousa_tip=$(git -C "$source_repository" rev-parse HEAD) + +git -C "$source_repository" switch --quiet --detach "$lmsousa_tip" +git -C "$source_repository" switch --quiet -c unrelated-sibling +commit "unrelated sibling branch" +unrelated_sibling_tip=$(git -C "$source_repository" rev-parse HEAD) +git -C "$source_repository" switch --quiet --detach "$workflow_fix_tip" + +for branch in workflow-fix java-deprecation vitest lmsousa staging master; do + branch_variable=${branch//-/_}_tip + git -C "$source_repository" update-ref \ + "refs/remotes/origin/${branch}" "${!branch_variable}" +done +git -C "$source_repository" update-ref \ + refs/remotes/origin/workflow-fix "$post_event_source_tip" +git -C "$source_repository" update-ref \ + refs/remotes/origin/later-parent "$later_parent_tip" +# Clava's vitest and java-deprecation refs are intentionally equal. Another +# repository must provide their strict ordering. +git -C "$source_repository" update-ref \ + refs/remotes/origin/vitest "$java_deprecation_tip" + +make_dependency() { + local name=$1 + shift + local repository="${test_directory}/${name}.git" + + git clone --quiet --bare "$source_repository" "$repository" + while IFS= read -r ref; do + git -C "$repository" update-ref -d "$ref" + done < <(git -C "$repository" for-each-ref --format='%(refname)' refs/heads) + + git -C "$repository" update-ref refs/heads/master "$master_tip" + for branch in "$@"; do + branch_variable=${branch//-/_}_tip + git -C "$repository" update-ref "refs/heads/${branch}" "${!branch_variable}" + done + git -C "$repository" symbolic-ref HEAD refs/heads/master + echo "$repository" +} + +current_dependency=$(make_dependency current workflow-fix lmsousa) +middle_dependency=$(make_dependency middle java-deprecation vitest lmsousa) +older_dependency=$(make_dependency older vitest lmsousa) +integration_dependency=$(make_dependency integration staging) +root_dependency=$(make_dependency root) +equal_dependency=$(make_dependency equal java-deprecation vitest lmsousa) +git -C "$equal_dependency" update-ref \ + refs/heads/vitest "$java_deprecation_tip" +ambiguous_dependency=$(make_dependency ambiguous java-deprecation vitest lmsousa) +git -C "$ambiguous_dependency" update-ref \ + refs/heads/vitest "$staging_tip" +reverse_dependency=$(make_dependency reverse java-deprecation vitest lmsousa) +git -C "$reverse_dependency" update-ref \ + refs/heads/java-deprecation "$vitest_tip" +git -C "$reverse_dependency" update-ref \ + refs/heads/vitest "$java_deprecation_tip" +advanced_dependency=$(make_dependency advanced lmsousa staging) +git -C "$advanced_dependency" update-ref \ + refs/heads/lmsousa "$advanced_lmsousa_tip" +rebased_dependency=$(make_dependency rebased lmsousa staging) +git -C "$rebased_dependency" update-ref \ + refs/heads/lmsousa "$rebased_lmsousa_tip" +sibling_dependency=$(make_dependency sibling unrelated-sibling staging) + +output_file="${test_directory}/github-output" +log_file="${test_directory}/resolver.log" +SOURCE_BRANCH=workflow-fix GITHUB_OUTPUT="$output_file" \ + bash "$resolver" "$source_repository" \ + current "$current_dependency" \ + middle "$middle_dependency" \ + older "$older_dependency" \ + integration "$integration_dependency" \ + root "$root_dependency" | + tee "$log_file" + +grep -Fqx \ + "Dependency branch candidates: workflow-fix -> java-deprecation -> vitest -> lmsousa -> staging -> master" \ + "$log_file" +if grep -Fq "later-parent" "$log_file"; then + echo "Resolver used a branch created after the workflow event" >&2 + exit 1 +fi +grep -Fqx "current_branch=workflow-fix" "$output_file" +grep -Fqx "current_ref=${workflow_fix_tip}" "$output_file" +grep -Fqx "middle_branch=java-deprecation" "$output_file" +grep -Fqx "middle_ref=${java_deprecation_tip}" "$output_file" +grep -Fqx "older_branch=vitest" "$output_file" +grep -Fqx "older_ref=${vitest_tip}" "$output_file" +grep -Fqx "integration_branch=staging" "$output_file" +grep -Fqx "integration_ref=${staging_tip}" "$output_file" +grep -Fqx "root_branch=master" "$output_file" +grep -Fqx "root_ref=${master_tip}" "$output_file" + +# Equal, unordered names are safe when they resolve to the same target commit. +equal_output="${test_directory}/equal-output" +SOURCE_BRANCH=workflow-fix GITHUB_OUTPUT="$equal_output" \ + bash "$resolver" "$source_repository" \ + equal "$equal_dependency" >/dev/null +grep -Fqx "equal_ref=${java_deprecation_tip}" "$equal_output" + +# Diverged candidates with no ordering evidence must fail rather than guess. +if SOURCE_BRANCH=workflow-fix GITHUB_OUTPUT="${test_directory}/ambiguous-output" \ + bash "$resolver" "$source_repository" \ + ambiguous "$ambiguous_dependency" \ + >"${test_directory}/ambiguous.log" 2>&1; then + echo "Resolver accepted ambiguous, different dependency refs" >&2 + exit 1 +fi +grep -Fq "Ambiguous newest branches" "${test_directory}/ambiguous.log" + +# Opposite strict orderings from two repositories are contradictory. +if SOURCE_BRANCH=workflow-fix GITHUB_OUTPUT="${test_directory}/contradictory-output" \ + bash "$resolver" "$source_repository" \ + forward "$middle_dependency" \ + reverse "$reverse_dependency" \ + >"${test_directory}/contradictory.log" 2>&1; then + echo "Resolver accepted contradictory repository evidence" >&2 + exit 1 +fi +grep -Fq "Contradictory branch ordering evidence" \ + "${test_directory}/contradictory.log" + +# Terminal branch workflows must not create a staging/master ordering cycle. +git -C "$source_repository" switch --quiet --detach "$staging_tip" +SOURCE_BRANCH=staging GITHUB_OUTPUT="${test_directory}/staging-output" \ + bash "$resolver" "$source_repository" \ + integration "$integration_dependency" >/dev/null +grep -Fqx "integration_ref=${staging_tip}" \ + "${test_directory}/staging-output" + +git -C "$source_repository" switch --quiet --detach "$master_tip" +SOURCE_BRANCH=master GITHUB_OUTPUT="${test_directory}/master-output" \ + bash "$resolver" "$source_repository" \ + root "$root_dependency" >/dev/null +grep -Fqx "root_ref=${master_tip}" "${test_directory}/master-output" + +# A shared parent remains a candidate when its tip advances or is rebased, +# provided its merge-base still identifies this post-master stack. +git -C "$source_repository" switch --quiet --detach "$workflow_fix_tip" +git -C "$source_repository" update-ref \ + refs/remotes/origin/lmsousa "$advanced_lmsousa_tip" +SOURCE_BRANCH=workflow-fix EVIDENCE_REPOSITORIES="$middle_dependency" \ + GITHUB_OUTPUT="${test_directory}/advanced-output" \ + bash "$resolver" "$source_repository" \ + advanced "$advanced_dependency" >/dev/null +grep -Fqx "advanced_ref=${advanced_lmsousa_tip}" \ + "${test_directory}/advanced-output" + +git -C "$source_repository" update-ref \ + refs/remotes/origin/lmsousa "$rebased_lmsousa_tip" +SOURCE_BRANCH=workflow-fix EVIDENCE_REPOSITORIES="$middle_dependency" \ + GITHUB_OUTPUT="${test_directory}/rebased-output" \ + bash "$resolver" "$source_repository" \ + rebased "$rebased_dependency" >/dev/null +grep -Fqx "rebased_ref=${rebased_lmsousa_tip}" \ + "${test_directory}/rebased-output" + +# A shared sibling with the same fork point is not a proven stack member. +git -C "$source_repository" update-ref \ + refs/remotes/origin/lmsousa "$lmsousa_tip" +git -C "$source_repository" update-ref \ + refs/remotes/origin/unrelated-sibling "$unrelated_sibling_tip" +if SOURCE_BRANCH=workflow-fix GITHUB_OUTPUT="${test_directory}/sibling-output" \ + bash "$resolver" "$source_repository" \ + sibling "$sibling_dependency" \ + >"${test_directory}/sibling.log" 2>&1; then + echo "Resolver selected an unproven sibling branch" >&2 + exit 1 +fi +grep -Fq "Cannot place shared branch 'unrelated-sibling'" \ + "${test_directory}/sibling.log" + +echo "All dependency ref resolver tests passed" diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index d6a802310..b6c2c1cff 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -9,9 +9,13 @@ on: push: paths: - .github/workflows/copilot-setup-steps.yml + - .github/scripts/resolve-dependency-refs.sh + - .github/scripts/test-resolve-dependency-refs.sh pull_request: paths: - .github/workflows/copilot-setup-steps.yml + - .github/scripts/resolve-dependency-refs.sh + - .github/scripts/test-resolve-dependency-refs.sh env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} @@ -46,62 +50,31 @@ jobs: uses: actions/checkout@v6 with: path: clava + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Test dependency ref resolver + run: bash clava/.github/scripts/test-resolve-dependency-refs.sh - name: Determine repository refs id: repo-refs shell: bash env: - BRANCH_NAME: ${{ env.BRANCH_NAME }} - BASE_BRANCH: ${{ github.base_ref }} + SOURCE_BRANCH: ${{ env.BRANCH_NAME }} run: | - set -euo pipefail - - # For each dependency repository, determine which branch to checkout. - # Priority order: - # 1. A branch with the same name as the current branch - # 2. If this is a PR, the target branch (base_ref) - # 3. The default branch of the repository - - determine_ref() { - local prefix=$1 - local repo=$2 - local url="https://github.com/${repo}.git" - - # Get the default branch - local default_branch - default_branch=$(git ls-remote --symref "$url" HEAD | awk '/^ref:/ {print $2}' | sed 's@refs/heads/@@') - echo "${prefix}_default=${default_branch}" >> "$GITHUB_OUTPUT" - echo "Default branch for ${repo} is '${default_branch}'" - - local ref_to_use="" - - # Priority 1: Same branch name - if [ -n "$(git ls-remote --heads "$url" "refs/heads/${BRANCH_NAME}")" ]; then - ref_to_use="${BRANCH_NAME}" - echo "Using matching branch '${BRANCH_NAME}' in ${repo}" - # Priority 2: PR target branch (if this is a PR) - elif [ -n "${BASE_BRANCH}" ] && [ -n "$(git ls-remote --heads "$url" "refs/heads/${BASE_BRANCH}")" ]; then - ref_to_use="${BASE_BRANCH}" - echo "Using PR target branch '${BASE_BRANCH}' in ${repo}" - # Priority 3: Default branch - else - ref_to_use="${default_branch}" - echo "Using default branch '${default_branch}' for ${repo}" - fi - - echo "${prefix}_ref=${ref_to_use}" >> "$GITHUB_OUTPUT" - } - - determine_ref "lara" "specs-feup/lara-framework" - determine_ref "specs" "specs-feup/specs-java-libs" + bash clava/.github/scripts/resolve-dependency-refs.sh \ + clava \ + lara specs-feup/lara-framework \ + specs specs-feup/specs-java-libs - name: Echo checks run: | echo "Weaver branch: ${{ env.BRANCH_NAME }}" - echo "PR target branch (if any): ${{ github.base_ref }}" - echo "Lara framework ref: ${{ steps.repo-refs.outputs.lara_ref }}" + echo "Lara framework branch: ${{ steps.repo-refs.outputs.lara_branch }}" + echo "Lara framework commit: ${{ steps.repo-refs.outputs.lara_ref }}" echo "Lara framework default: ${{ steps.repo-refs.outputs.lara_default }}" - echo "Specs-java-libs ref: ${{ steps.repo-refs.outputs.specs_ref }}" + echo "Specs-java-libs branch: ${{ steps.repo-refs.outputs.specs_branch }}" + echo "Specs-java-libs commit: ${{ steps.repo-refs.outputs.specs_ref }}" echo "Specs-java-libs default: ${{ steps.repo-refs.outputs.specs_default }}" - name: Checkout lara-framework diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 98915e2b0..f4e09490e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -7,6 +7,7 @@ name: nightly on: push: + pull_request: # Daily at midnight schedule: @@ -46,62 +47,31 @@ jobs: uses: actions/checkout@v6 with: path: clava + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Test dependency ref resolver + run: bash clava/.github/scripts/test-resolve-dependency-refs.sh - name: Determine repository refs id: repo-refs shell: bash env: - BRANCH_NAME: ${{ env.BRANCH_NAME }} - BASE_BRANCH: ${{ github.base_ref }} + SOURCE_BRANCH: ${{ env.BRANCH_NAME }} run: | - set -euo pipefail - - # For each dependency repository, determine which branch to checkout. - # Priority order: - # 1. A branch with the same name as the current branch - # 2. If this is a PR, the target branch (base_ref) - # 3. The default branch of the repository - - determine_ref() { - local prefix=$1 - local repo=$2 - local url="https://github.com/${repo}.git" - - # Get the default branch - local default_branch - default_branch=$(git ls-remote --symref "$url" HEAD | awk '/^ref:/ {print $2}' | sed 's@refs/heads/@@') - echo "${prefix}_default=${default_branch}" >> "$GITHUB_OUTPUT" - echo "Default branch for ${repo} is '${default_branch}'" - - local ref_to_use="" - - # Priority 1: Same branch name - if [ -n "$(git ls-remote --heads "$url" "refs/heads/${BRANCH_NAME}")" ]; then - ref_to_use="${BRANCH_NAME}" - echo "Using matching branch '${BRANCH_NAME}' in ${repo}" - # Priority 2: PR target branch (if this is a PR) - elif [ -n "${BASE_BRANCH}" ] && [ -n "$(git ls-remote --heads "$url" "refs/heads/${BASE_BRANCH}")" ]; then - ref_to_use="${BASE_BRANCH}" - echo "Using PR target branch '${BASE_BRANCH}' in ${repo}" - # Priority 3: Default branch - else - ref_to_use="${default_branch}" - echo "Using default branch '${default_branch}' for ${repo}" - fi - - echo "${prefix}_ref=${ref_to_use}" >> "$GITHUB_OUTPUT" - } - - determine_ref "lara" "specs-feup/lara-framework" - determine_ref "specs" "specs-feup/specs-java-libs" + bash clava/.github/scripts/resolve-dependency-refs.sh \ + clava \ + lara specs-feup/lara-framework \ + specs specs-feup/specs-java-libs - name: Echo checks run: | echo "Weaver branch: ${{ env.BRANCH_NAME }}" - echo "PR target branch (if any): ${{ github.base_ref }}" - echo "Lara framework ref: ${{ steps.repo-refs.outputs.lara_ref }}" + echo "Lara framework branch: ${{ steps.repo-refs.outputs.lara_branch }}" + echo "Lara framework commit: ${{ steps.repo-refs.outputs.lara_ref }}" echo "Lara framework default: ${{ steps.repo-refs.outputs.lara_default }}" - echo "Specs-java-libs ref: ${{ steps.repo-refs.outputs.specs_ref }}" + echo "Specs-java-libs branch: ${{ steps.repo-refs.outputs.specs_branch }}" + echo "Specs-java-libs commit: ${{ steps.repo-refs.outputs.specs_ref }}" echo "Specs-java-libs default: ${{ steps.repo-refs.outputs.specs_default }}" - name: Checkout lara-framework diff --git a/Clava-JS/api/SourceLocations.test.ts b/Clava-JS/api/SourceLocations.test.ts index d2d06f95a..c805d530c 100644 --- a/Clava-JS/api/SourceLocations.test.ts +++ b/Clava-JS/api/SourceLocations.test.ts @@ -1,4 +1,4 @@ -import { registerSourceCode } from "@specs-feup/lara/jest/jestHelpers.ts"; +import { registerSourceCode } from "@specs-feup/lara/vitest/weaverTestHelpers.ts"; import Query from "@specs-feup/lara/api/weaver/Query.ts"; import { Joinpoint, Vardecl } from "./Joinpoints.ts";