blob: b32b219c3b65f8b0a96c9f59babeae916d035d8d [file]
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
# check-placeholders.sh
#
# Verifies that framework-level skill / tool docs refer to
# adopting-project specifics through placeholders only:
#
# <PROJECT> — adopting project's display name
# <tracker> — adopting project's private tracker repo slug
# <upstream> — adopting project's public source repo slug
#
# Hardcoded references to "apache/airflow", "airflow-s/airflow-s",
# "Apache Airflow", or related concrete project names slip in
# whenever someone copy-pastes from the legacy Airflow content.
# This linter catches them before merge.
#
# Intentional callouts that **explain** the placeholder convention
# (example blocks, "for Airflow, see..." pointers, title-prefix
# rendering examples) are allowlisted via inline markers
# (`example:`, `e.g.`, `for Airflow`, lines inside `<!-- ... -->`
# HTML comment blocks). Regular prose that names Airflow without
# such a marker is the surface this linter is built to catch.
#
# Run from repo root:
# tools/dev/check-placeholders.sh
#
# Pre-commit invocation: see `.pre-commit-config.yaml`.
set -euo pipefail
# Patterns that should never appear outside the allowlist below.
# Each pattern must be a fixed string (grep -F).
FORBIDDEN_PATTERNS=(
"apache/airflow"
"airflow-s/airflow-s"
"Apache Airflow"
"apache.org/airflow"
)
# Files / directories where Airflow references are intentional:
# the framework's own onboarding / contributor docs use Airflow as
# the canonical example adopter; the bootstrap scaffold under
# `projects/_template/` may reference Airflow in pointers; this
# linter file itself contains the patterns to look for.
ALLOWLIST_PATHS=(
"README.md"
"AGENTS.md"
"CONTRIBUTING.md"
"docs/setup/secure-agent-setup.md"
"docs/security/how-to-fix-a-security-issue.md"
"docs/security/new-members-onboarding.md"
"pyproject.toml"
"projects/_template/"
"organizations/"
"tools/dev/check-placeholders.sh"
".github/"
".asf.yaml"
"NOTICE"
"LICENSE"
# Eval fixture reports simulate real inbound emails and must contain
# the concrete project name — they are not skill / tool docs.
"tools/skill-evals/evals/"
)
# Inline markers that signal an intentional explanatory mention
# of Airflow on the same line. Lines matching any of these are
# treated as allowlisted.
# Entries are matched as literal bash substrings, not regex patterns.
INLINE_ALLOW_MARKERS=(
"example:"
"e.g."
"for Airflow"
"the Airflow"
"legacy"
"renamed"
"future-renamed"
"originally"
"vendor>: <product>"
)
# Where to look. Only `.md` files under skills + tool adapter docs
# are scoped; Python sources under `tools/*/src/` and `tools/*/tests/`
# may legitimately mention Airflow in fixtures and docstrings.
SCAN_PATHS=(
"skills"
"tools"
)
is_path_allowlisted() {
local file="$1"
for allow in "${ALLOWLIST_PATHS[@]}"; do
if [[ "$file" == "$allow"* ]]; then
return 0
fi
done
return 1
}
line_has_inline_allow_marker() {
local line="$1"
for marker in "${INLINE_ALLOW_MARKERS[@]}"; do
if [[ "$line" == *"$marker"* ]]; then
return 0
fi
done
return 1
}
main() {
local repo_root
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$repo_root"
local exit_code=0
local matches_total=0
echo "check-placeholders: scanning ${SCAN_PATHS[*]} for hardcoded project references..."
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
local matches
matches=$(grep -rFn \
--include='*.md' \
"$pattern" \
"${SCAN_PATHS[@]}" 2>/dev/null || true)
if [[ -z "$matches" ]]; then
continue
fi
while IFS= read -r match_line; do
local file="${match_line%%:*}"
local rest="${match_line#*:}"
local _line_no="${rest%%:*}"
local content="${rest#*:}"
if is_path_allowlisted "$file"; then
continue
fi
if line_has_inline_allow_marker "$content"; then
continue
fi
if [[ $matches_total -eq 0 ]]; then
{
echo ""
echo "FORBIDDEN: hardcoded project references found."
echo ""
echo "Skill / tool docs must use the placeholders <PROJECT>,"
echo "<tracker>, and <upstream> instead of the concrete strings"
echo "below. See AGENTS.md#placeholder-convention-used-in-skill-files."
echo ""
echo "Lines that explain the placeholder convention with an"
echo "intentional example are allowlisted by including one of:"
echo " example:, e.g., for Airflow, the Airflow, legacy,"
echo " renamed, future-renamed, originally, vendor>: <product>"
echo ""
} >&2
fi
echo " $match_line" >&2
matches_total=$((matches_total + 1))
exit_code=1
done <<< "$matches"
done
if [[ $exit_code -eq 0 ]]; then
echo "check-placeholders: OK (no hardcoded references in skills / tool docs)."
else
echo "" >&2
echo "check-placeholders: $matches_total violation(s) — fix before commit." >&2
fi
return $exit_code
}
main "$@"