486 lines
16 KiB
Bash
Executable File
486 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
format="human"
|
|
cli_family=""
|
|
work_classification=""
|
|
pull_request_target=""
|
|
spec_path=""
|
|
branch_name=""
|
|
repo_scope=""
|
|
explicit_ref=""
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/resolve-feature-base [options]
|
|
|
|
Options:
|
|
--branch-family=platform|website|repository-wide
|
|
--work-classification=platform|website|cross-stream|promotion|repository-governance
|
|
--pull-request-target=<platform-dev|website-dev|dev>
|
|
--spec=<path-to-spec.md>
|
|
--branch-name=<feature-branch>
|
|
--repo-scope=platform|website|repository
|
|
--ref=<family-matching-git-ref>
|
|
--format=human|kv
|
|
EOF
|
|
}
|
|
|
|
fail_resolution() {
|
|
local reason="$1"
|
|
shift
|
|
|
|
if [[ "${format}" == "human" ]]; then
|
|
echo "Feature base resolution failed." >&2
|
|
fi
|
|
|
|
echo "status=failed" >&2
|
|
echo "reason=${reason}" >&2
|
|
|
|
for detail in "$@"; do
|
|
echo "${detail}" >&2
|
|
done
|
|
|
|
echo "guidance=Declare a consistent branch family, work classification, PR target, active spec, and family-matching ref." >&2
|
|
echo "diff_ran=false" >&2
|
|
exit 2
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--branch-family=*)
|
|
cli_family="${1#*=}"
|
|
shift
|
|
;;
|
|
--branch-family)
|
|
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--branch-family"
|
|
cli_family="$2"
|
|
shift 2
|
|
;;
|
|
--work-classification=*)
|
|
work_classification="${1#*=}"
|
|
shift
|
|
;;
|
|
--work-classification)
|
|
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--work-classification"
|
|
work_classification="$2"
|
|
shift 2
|
|
;;
|
|
--pull-request-target=*)
|
|
pull_request_target="${1#*=}"
|
|
shift
|
|
;;
|
|
--pull-request-target)
|
|
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--pull-request-target"
|
|
pull_request_target="$2"
|
|
shift 2
|
|
;;
|
|
--spec=*)
|
|
spec_path="${1#*=}"
|
|
shift
|
|
;;
|
|
--spec)
|
|
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--spec"
|
|
spec_path="$2"
|
|
shift 2
|
|
;;
|
|
--branch-name=*)
|
|
branch_name="${1#*=}"
|
|
shift
|
|
;;
|
|
--branch-name)
|
|
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--branch-name"
|
|
branch_name="$2"
|
|
shift 2
|
|
;;
|
|
--repo-scope=*)
|
|
repo_scope="${1#*=}"
|
|
shift
|
|
;;
|
|
--repo-scope)
|
|
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--repo-scope"
|
|
repo_scope="$2"
|
|
shift 2
|
|
;;
|
|
--ref=*)
|
|
explicit_ref="${1#*=}"
|
|
shift
|
|
;;
|
|
--ref)
|
|
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--ref"
|
|
explicit_ref="$2"
|
|
shift 2
|
|
;;
|
|
--format=*)
|
|
format="${1#*=}"
|
|
shift
|
|
;;
|
|
--format)
|
|
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--format"
|
|
format="$2"
|
|
shift 2
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
fail_resolution "unknown-option" "option=$1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "${format}" != "human" && "${format}" != "kv" ]]; then
|
|
fail_resolution "invalid-format" "format=${format}"
|
|
fi
|
|
|
|
normalize_family() {
|
|
case "$1" in
|
|
platform)
|
|
echo "platform"
|
|
;;
|
|
website)
|
|
echo "website"
|
|
;;
|
|
repository-wide)
|
|
echo "repository-wide"
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
family_for_branch() {
|
|
case "$1" in
|
|
platform-dev|origin/platform-dev|refs/heads/platform-dev|refs/remotes/origin/platform-dev)
|
|
echo "platform"
|
|
;;
|
|
website-dev|origin/website-dev|refs/heads/website-dev|refs/remotes/origin/website-dev)
|
|
echo "website"
|
|
;;
|
|
dev|origin/dev|refs/heads/dev|refs/remotes/origin/dev)
|
|
echo "repository-wide"
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
expected_branch_for_family() {
|
|
case "$1" in
|
|
platform)
|
|
echo "platform-dev"
|
|
;;
|
|
website)
|
|
echo "website-dev"
|
|
;;
|
|
repository-wide)
|
|
echo "dev"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
trim_value() {
|
|
local value="$1"
|
|
value="${value#"${value%%[![:space:]]*}"}"
|
|
value="${value%"${value##*[![:space:]]}"}"
|
|
value="${value//\`/}"
|
|
printf '%s\n' "${value}"
|
|
}
|
|
|
|
metadata_value() {
|
|
local file="$1"
|
|
local label="$2"
|
|
local line
|
|
|
|
line="$(awk -v prefix="**${label}**:" 'index($0, prefix) == 1 { print substr($0, length(prefix) + 1); exit }' "${file}")"
|
|
trim_value "${line}"
|
|
}
|
|
|
|
repository_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
if [[ -z "${repository_root}" ]]; then
|
|
fail_resolution "not-a-git-repository"
|
|
fi
|
|
|
|
if [[ -z "${branch_name}" ]]; then
|
|
branch_name="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
|
|
fi
|
|
|
|
environment_family="${TENANTATLAS_BRANCH_FAMILY:-}"
|
|
if [[ -n "${cli_family}" && -n "${environment_family}" && "${cli_family}" != "${environment_family}" ]]; then
|
|
fail_resolution "branch-family-conflict" "cli_family=${cli_family}" "workflow_family=${environment_family}"
|
|
fi
|
|
|
|
explicit_family_raw="${cli_family:-${environment_family}}"
|
|
explicit_family=""
|
|
if [[ -n "${explicit_family_raw}" ]]; then
|
|
explicit_family="$(normalize_family "${explicit_family_raw}" || true)"
|
|
if [[ -z "${explicit_family}" ]]; then
|
|
fail_resolution "invalid-branch-family" "explicit=${explicit_family_raw}"
|
|
fi
|
|
fi
|
|
|
|
environment_classification="${TENANTATLAS_WORK_CLASSIFICATION:-}"
|
|
if [[ -n "${work_classification}" && -n "${environment_classification}" && "${work_classification}" != "${environment_classification}" ]]; then
|
|
fail_resolution "work-classification-conflict" "cli_classification=${work_classification}" "workflow_classification=${environment_classification}"
|
|
fi
|
|
work_classification="${work_classification:-${environment_classification}}"
|
|
|
|
case "${work_classification}" in
|
|
""|platform|website|cross-stream|promotion|repository-governance)
|
|
;;
|
|
*)
|
|
fail_resolution "invalid-work-classification" "work_classification=${work_classification}"
|
|
;;
|
|
esac
|
|
|
|
environment_ref="${TENANTATLAS_BASE_REF:-}"
|
|
if [[ -n "${explicit_ref}" && -n "${environment_ref}" && "${explicit_ref}" != "${environment_ref}" ]]; then
|
|
fail_resolution "explicit-ref-conflict" "cli_ref=${explicit_ref}" "workflow_ref=${environment_ref}"
|
|
fi
|
|
explicit_ref="${explicit_ref:-${environment_ref}}"
|
|
|
|
resolved_target=""
|
|
target_family=""
|
|
for target_signal in \
|
|
"${pull_request_target}" \
|
|
"${TENANTATLAS_PR_TARGET:-}" \
|
|
"${GITHUB_BASE_REF:-}" \
|
|
"${GITEA_BASE_REF:-}"; do
|
|
[[ -z "${target_signal}" ]] && continue
|
|
|
|
signal_family="$(family_for_branch "${target_signal}" || true)"
|
|
if [[ -z "${signal_family}" ]]; then
|
|
fail_resolution "unknown-pull-request-target" "pull_request_target=${target_signal}"
|
|
fi
|
|
|
|
if [[ -z "${resolved_target}" ]]; then
|
|
resolved_target="${target_signal}"
|
|
target_family="${signal_family}"
|
|
elif [[ "${target_family}" != "${signal_family}" ]]; then
|
|
fail_resolution "pull-request-target-conflict" "pull_request_targets=${resolved_target},${target_signal}"
|
|
fi
|
|
done
|
|
|
|
explicit_spec_requested=false
|
|
if [[ -n "${spec_path}" ]]; then
|
|
explicit_spec_requested=true
|
|
elif [[ -n "${TENANTATLAS_ACTIVE_SPEC:-}" ]]; then
|
|
spec_path="${TENANTATLAS_ACTIVE_SPEC}"
|
|
explicit_spec_requested=true
|
|
elif [[ "${branch_name}" == feat/* ]]; then
|
|
candidate_spec="${repository_root}/specs/${branch_name#feat/}/spec.md"
|
|
if [[ -f "${candidate_spec}" ]]; then
|
|
spec_path="${candidate_spec}"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "${spec_path}" && "${spec_path}" != /* ]]; then
|
|
spec_path="${repository_root}/${spec_path}"
|
|
fi
|
|
|
|
if [[ "${explicit_spec_requested}" == true && ! -f "${spec_path}" ]]; then
|
|
fail_resolution "spec-not-readable" "spec_path=${spec_path}"
|
|
fi
|
|
|
|
spec_family=""
|
|
spec_classification=""
|
|
spec_declared_target=""
|
|
if [[ -n "${spec_path}" && -f "${spec_path}" ]]; then
|
|
feature_base="$(metadata_value "${spec_path}" "Feature Base")"
|
|
merge_target="$(metadata_value "${spec_path}" "Normal Merge Target")"
|
|
spec_declared_target="${merge_target}"
|
|
development_stream="$(metadata_value "${spec_path}" "Development Stream")"
|
|
|
|
for metadata_branch in "${feature_base}" "${merge_target}"; do
|
|
[[ -z "${metadata_branch}" ]] && continue
|
|
metadata_family="$(family_for_branch "${metadata_branch}" || true)"
|
|
if [[ -z "${metadata_family}" ]]; then
|
|
fail_resolution "unknown-spec-branch" "spec_branch=${metadata_branch}" "spec_path=${spec_path}"
|
|
fi
|
|
if [[ -z "${spec_family}" ]]; then
|
|
spec_family="${metadata_family}"
|
|
elif [[ "${spec_family}" != "${metadata_family}" ]]; then
|
|
fail_resolution "spec-metadata-conflict" "feature_base=${feature_base}" "normal_merge_target=${merge_target}" "spec_path=${spec_path}"
|
|
fi
|
|
done
|
|
|
|
case "${development_stream}" in
|
|
platform*) spec_classification="platform" ;;
|
|
website*) spec_classification="website" ;;
|
|
cross-stream*) spec_classification="cross-stream" ;;
|
|
promotion*) spec_classification="promotion" ;;
|
|
repository-governance*) spec_classification="repository-governance" ;;
|
|
esac
|
|
fi
|
|
|
|
if [[ -n "${work_classification}" && -n "${spec_classification}" && "${work_classification}" != "${spec_classification}" ]]; then
|
|
fail_resolution "work-classification-conflict" "explicit_classification=${work_classification}" "spec_classification=${spec_classification}"
|
|
fi
|
|
work_classification="${work_classification:-${spec_classification}}"
|
|
|
|
branch_hint=""
|
|
lower_branch="$(printf '%s' "${branch_name}" | tr '[:upper:]' '[:lower:]')"
|
|
if [[ "${lower_branch}" == *platform* && "${lower_branch}" == *website* ]]; then
|
|
branch_hint="ambiguous"
|
|
elif [[ "${lower_branch}" == *platform* ]]; then
|
|
branch_hint="platform"
|
|
elif [[ "${lower_branch}" == *website* ]]; then
|
|
branch_hint="website"
|
|
fi
|
|
|
|
scope_hint=""
|
|
repo_scope="${repo_scope:-${TENANTATLAS_REPO_SCOPE:-}}"
|
|
case "${repo_scope}" in
|
|
""|repository)
|
|
;;
|
|
platform|website)
|
|
scope_hint="${repo_scope}"
|
|
;;
|
|
*)
|
|
fail_resolution "invalid-repository-scope" "repo_scope=${repo_scope}"
|
|
;;
|
|
esac
|
|
|
|
hint_family=""
|
|
hint_ambiguous=false
|
|
if [[ "${branch_hint}" == "ambiguous" ]]; then
|
|
hint_ambiguous=true
|
|
elif [[ -n "${branch_hint}" && -n "${scope_hint}" ]]; then
|
|
if [[ "${branch_hint}" != "${scope_hint}" ]]; then
|
|
hint_ambiguous=true
|
|
else
|
|
hint_family="${branch_hint}"
|
|
fi
|
|
fi
|
|
|
|
selected_family=""
|
|
family_source=""
|
|
|
|
if [[ -n "${explicit_family}" ]]; then
|
|
selected_family="${explicit_family}"
|
|
family_source="explicit"
|
|
fi
|
|
|
|
if [[ -n "${target_family}" ]]; then
|
|
if [[ -n "${selected_family}" && "${selected_family}" != "${target_family}" ]]; then
|
|
fail_resolution "branch-family-conflict" "explicit=${selected_family}" "pull_request_target=${target_family}"
|
|
fi
|
|
if [[ -z "${selected_family}" ]]; then
|
|
selected_family="${target_family}"
|
|
family_source="pull-request-target"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "${spec_family}" ]]; then
|
|
if [[ -n "${selected_family}" && "${selected_family}" != "${spec_family}" ]]; then
|
|
fail_resolution "branch-family-conflict" "selected=${selected_family}" "spec_metadata=${spec_family}" "spec_path=${spec_path}"
|
|
fi
|
|
if [[ -z "${selected_family}" ]]; then
|
|
selected_family="${spec_family}"
|
|
family_source="spec-metadata"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "${hint_family}" ]]; then
|
|
if [[ -z "${selected_family}" ]]; then
|
|
selected_family="${hint_family}"
|
|
family_source="branch-and-scope"
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "${selected_family}" ]]; then
|
|
if [[ "${hint_ambiguous}" == true ]]; then
|
|
fail_resolution "ambiguous-branch-family" "branch_name=${branch_name}" "repo_scope=${repo_scope:-unset}" "spec_path=${spec_path:-unset}"
|
|
fi
|
|
fail_resolution "ambiguous-branch-family" "branch_name=${branch_name:-detached}" "repo_scope=${repo_scope:-unset}" "spec_path=${spec_path:-unset}"
|
|
fi
|
|
|
|
if [[ -z "${work_classification}" ]]; then
|
|
if [[ "${selected_family}" == "repository-wide" ]]; then
|
|
fail_resolution "explicit-promotion-classification-required" "pull_request_target=${resolved_target:-unset}"
|
|
fi
|
|
work_classification="${selected_family}"
|
|
fi
|
|
|
|
case "${work_classification}:${selected_family}" in
|
|
platform:platform|website:website|promotion:repository-wide|cross-stream:*|repository-governance:*)
|
|
;;
|
|
*)
|
|
fail_resolution "work-classification-family-conflict" "work_classification=${work_classification}" "branch_family=${selected_family}"
|
|
;;
|
|
esac
|
|
|
|
if [[ "${selected_family}" == "repository-wide" && "${work_classification}" != "promotion" && "${work_classification}" != "cross-stream" && "${work_classification}" != "repository-governance" ]]; then
|
|
fail_resolution "explicit-promotion-classification-required" "work_classification=${work_classification}"
|
|
fi
|
|
|
|
if [[ "${work_classification}" == "promotion" || "${work_classification}" == "cross-stream" || "${work_classification}" == "repository-governance" ]]; then
|
|
if [[ -z "${resolved_target}" && -z "${spec_declared_target}" ]]; then
|
|
fail_resolution "explicit-target-required" "work_classification=${work_classification}" "branch_family=${selected_family}"
|
|
fi
|
|
fi
|
|
|
|
expected_branch="$(expected_branch_for_family "${selected_family}")"
|
|
selected_ref=""
|
|
|
|
if [[ -n "${explicit_ref}" ]]; then
|
|
explicit_ref_family="$(family_for_branch "${explicit_ref}" || true)"
|
|
if [[ -z "${explicit_ref_family}" || "${explicit_ref_family}" != "${selected_family}" ]]; then
|
|
fail_resolution "wrong-family-ref" "expected_branch=${expected_branch}" "explicit_ref=${explicit_ref}"
|
|
fi
|
|
|
|
if ! git rev-parse --verify "${explicit_ref}^{commit}" >/dev/null 2>&1; then
|
|
fail_resolution "explicit-ref-unavailable" "expected_branch=${expected_branch}" "explicit_ref=${explicit_ref}"
|
|
fi
|
|
|
|
selected_ref="${explicit_ref}"
|
|
elif git rev-parse --verify "refs/remotes/origin/${expected_branch}^{commit}" >/dev/null 2>&1; then
|
|
selected_ref="origin/${expected_branch}"
|
|
elif git rev-parse --verify "refs/heads/${expected_branch}^{commit}" >/dev/null 2>&1; then
|
|
selected_ref="${expected_branch}"
|
|
else
|
|
fail_resolution "missing-family-ref" "branch_family=${selected_family}" "expected_branch=${expected_branch}" "attempted_refs=origin/${expected_branch},${expected_branch}"
|
|
fi
|
|
|
|
target_commit="$(git rev-parse --verify "${selected_ref}^{commit}" 2>/dev/null || true)"
|
|
if [[ -z "${target_commit}" ]]; then
|
|
fail_resolution "selected-ref-unavailable" "selected_ref=${selected_ref}"
|
|
fi
|
|
|
|
merge_base="$(git merge-base HEAD "${selected_ref}" 2>/dev/null || true)"
|
|
if [[ -z "${merge_base}" ]]; then
|
|
fail_resolution "no-common-merge-base" "selected_ref=${selected_ref}" "target_commit=${target_commit}"
|
|
fi
|
|
|
|
base_range="${merge_base}...HEAD"
|
|
|
|
if [[ "${format}" == "human" ]]; then
|
|
cat <<EOF
|
|
Feature base resolved.
|
|
Branch family: ${selected_family}
|
|
Work classification: ${work_classification}
|
|
Family source: ${family_source}
|
|
Expected integration branch: ${expected_branch}
|
|
Selected ref: ${selected_ref}
|
|
Target commit: ${target_commit}
|
|
Merge base: ${merge_base}
|
|
Base range: ${base_range}
|
|
EOF
|
|
else
|
|
cat <<EOF
|
|
status=resolved
|
|
branch_family=${selected_family}
|
|
work_classification=${work_classification}
|
|
family_source=${family_source}
|
|
expected_branch=${expected_branch}
|
|
selected_ref=${selected_ref}
|
|
target_commit=${target_commit}
|
|
merge_base=${merge_base}
|
|
base_range=${base_range}
|
|
EOF
|
|
fi
|