Files
Dariusz L 5d1930fe27 Make GitHub workflows reusable and validate inputs dynamically
- Replace hardcoded repository and token checks with generic default-branch and pyproject.toml validation
- Require fine-grained PAT permissions and validate Gist ownership and description
- Add concurrency groups to badge update workflows
- Bump softprops/action-gh-release to v3 and use DisplayName from pyproject.toml
2026-07-25 01:43:58 +02:00

287 lines
9.9 KiB
YAML

# Reusable workflow
#
# No project-specific repository variables need to be configured manually.
# The workflow creates and maintains this internal repository variable automatically:
# COMFY_CLONES_GIST_ID
#
# Required repository secret:
# SECRET_TOKEN Fine-grained personal access token with access to this repository:
# Repository permissions:
# Administration: Read
# Variables: Read and write
# Account permissions:
# Gists: Read and write
name: Update GitHub Clone Count Badge
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: write
concurrency:
group: badge-updates-${{ github.repository }}
cancel-in-progress: false
env:
BADGE_DOC: badges/CLONE.md
CLONE_GIST_ID: ${{ vars.COMFY_CLONES_GIST_ID }}
DATA_FILE: clone.json
PREVIOUS_DATA_FILE: clone_before.json
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v7
- name: Validate authentication
env:
GH_TOKEN: ${{ secrets.SECRET_TOKEN }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::error title=Missing SECRET_TOKEN::Add a fine-grained PAT with Administration: Read, Variables: Read and write, and Gists: Read and write."
exit 1
fi
gh auth status
- name: Fetch latest clone traffic
env:
GH_TOKEN: ${{ secrets.SECRET_TOKEN }}
run: |
if ! gh api \
-H "Accept: application/vnd.github+json" \
"repos/${GITHUB_REPOSITORY}/traffic/clones" > "$DATA_FILE"; then
echo "::error title=Cannot read clone traffic::SECRET_TOKEN needs Administration: Read access to this repository."
exit 1
fi
if ! jq -e '
type == "object" and
(.clones | type == "array")
' "$DATA_FILE" >/dev/null; then
echo "::error title=Invalid traffic response::GitHub did not return clone traffic data."
exit 1
fi
- name: Create or reuse clone statistics Gist
id: gist
env:
GH_TOKEN: ${{ secrets.SECRET_TOKEN }}
run: |
gist_id="$CLONE_GIST_ID"
expected_description="${GITHUB_REPOSITORY} clone statistics"
authenticated_user=$(gh api user --jq '.login')
save_gist_id=false
valid_gist=false
if [ -z "$gist_id" ]; then
gist_id=$(python - <<'PY'
from pathlib import Path
import os
import re
pattern = re.compile(
r"query=count.*?url=https://gist\.githubusercontent\.com/[^/]+/([0-9a-f]+)",
re.IGNORECASE,
)
for filename in (os.environ["BADGE_DOC"], "README.md"):
path = Path(filename)
if not path.is_file():
continue
match = pattern.search(path.read_text(encoding="utf-8"))
if match:
print(match.group(1))
break
PY
)
if [ -n "$gist_id" ]; then
echo "Found an existing clone statistics Gist in the badge documentation: $gist_id"
save_gist_id=true
fi
fi
if [ -n "$gist_id" ] && gh gist view "$gist_id" >/dev/null 2>&1; then
gist_owner=$(gh api "gists/$gist_id" --jq '.owner.login')
gist_description=$(gh api "gists/$gist_id" --jq '.description // ""')
if [ "$gist_owner" = "$authenticated_user" ] &&
[ "$gist_description" = "$expected_description" ]; then
valid_gist=true
else
echo "The detected Gist belongs to another repository or token owner."
fi
fi
if [ "$valid_gist" = true ]; then
echo "Using existing Gist: $gist_id"
else
if [ -n "$gist_id" ]; then
echo "Configured Gist does not exist or is inaccessible; creating a new one."
else
echo "No clone statistics Gist is configured; creating one."
fi
gist_url=$(gh gist create "$DATA_FILE" --desc "$expected_description")
gist_id="${gist_url##*/}"
save_gist_id=true
fi
if [ "$save_gist_id" = true ]; then
if ! gh variable set COMFY_CLONES_GIST_ID --body "$gist_id"; then
echo "::error title=Cannot save Gist ID::Grant SECRET_TOKEN repository Actions variables write access."
exit 1
fi
echo "Saved COMFY_CLONES_GIST_ID=$gist_id"
fi
if ! gh api "gists/$gist_id" \
--jq '.files["clone.json"].content // empty' > "$PREVIOUS_DATA_FILE"; then
cp "$DATA_FILE" "$PREVIOUS_DATA_FILE"
fi
if ! jq -e '
type == "object" and
(.clones | type == "array")
' "$PREVIOUS_DATA_FILE" >/dev/null 2>&1; then
echo "The Gist has no valid clone history yet; starting with the current data."
cp "$DATA_FILE" "$PREVIOUS_DATA_FILE"
fi
gist_owner=$(gh api "gists/$gist_id" --jq '.owner.login')
echo "id=$gist_id" >> "$GITHUB_OUTPUT"
echo "owner=$gist_owner" >> "$GITHUB_OUTPUT"
- name: Merge clone history
run: |
python - <<'PY'
import json
import os
from pathlib import Path
current_path = Path(os.environ["DATA_FILE"])
previous_path = Path(os.environ["PREVIOUS_DATA_FILE"])
current = json.loads(current_path.read_text(encoding="utf-8"))
previous = json.loads(previous_path.read_text(encoding="utf-8"))
by_timestamp = {}
for entry in previous.get("clones", []) + current.get("clones", []):
timestamp = str(entry.get("timestamp", "")).strip()
if timestamp:
by_timestamp[timestamp] = {
"timestamp": timestamp,
"count": int(entry.get("count", 0)),
"uniques": int(entry.get("uniques", 0)),
}
clones = [by_timestamp[key] for key in sorted(by_timestamp)]
if len(clones) > 100:
older = clones[:-35]
recent = clones[-35:]
monthly = {}
for entry in older:
month = entry["timestamp"][:7]
aggregate = monthly.setdefault(
month,
{"timestamp": month, "count": 0, "uniques": 0},
)
aggregate["count"] += entry["count"]
aggregate["uniques"] += entry["uniques"]
clones = [monthly[key] for key in sorted(monthly)] + recent
merged = dict(current)
merged["clones"] = clones
merged["count"] = sum(entry["count"] for entry in clones)
merged["uniques"] = sum(entry["uniques"] for entry in clones)
current_path.write_text(
json.dumps(merged, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
PY
jq . "$DATA_FILE"
- name: Update Gist and badge document
id: badge
env:
GH_TOKEN: ${{ secrets.SECRET_TOKEN }}
GIST_ID: ${{ steps.gist.outputs.id }}
GIST_OWNER: ${{ steps.gist.outputs.owner }}
run: |
content=$(jq -c . "$DATA_FILE")
jq -n \
--arg description "${GITHUB_REPOSITORY} clone statistics" \
--arg content "$content" \
'{
description: $description,
files: {
"clone.json": {
content: $content
}
}
}' > gist_patch.json
gh api --method PATCH "gists/${GIST_ID}" --input gist_patch.json >/dev/null
mkdir -p "$(dirname "$BADGE_DOC")"
gist_url="https://gist.githubusercontent.com/${GIST_OWNER}/${GIST_ID}/raw/clone.json"
repo_url="https://github.com/${GITHUB_REPOSITORY}"
shields="https://img.shields.io/badge/dynamic/json?color=success&label=Clone&query=count&url="
shields_for_the_badge="https://img.shields.io/badge/dynamic/json?color=2F80ED&label=Clone&query=count&url="
{
echo "**Markdown**"
echo
echo '```markdown'
echo "[![GitHub Clones](${shields}${gist_url}&logo=github)](${repo_url})"
echo '```'
echo
echo "[![GitHub Clones](${shields}${gist_url}&logo=github)](${repo_url})"
echo
echo "**HTML**"
echo
echo '```html'
echo "<a href='${repo_url}'><img alt='GitHub Clones' src='${shields}${gist_url}&logo=github'></a>"
echo '```'
echo
echo "<a href='${repo_url}'><img alt='GitHub Clones' src='${shields}${gist_url}&logo=github'></a>"
echo
echo "**HTML (for-the-badge)**"
echo
echo '```html'
echo "<a href='${repo_url}'><img alt='GitHub Clones' src='${shields_for_the_badge}${gist_url}&logo=github&style=for-the-badge'></a>"
echo '```'
echo
echo "<a href='${repo_url}'><img alt='GitHub Clones' src='${shields_for_the_badge}${gist_url}&logo=github&style=for-the-badge'></a>"
} > "$BADGE_DOC"
if [ -n "$(git status --porcelain -- "$BADGE_DOC")" ]; then
git add "$BADGE_DOC"
git config user.name "GitHub Action"
git config user.email "action@github.com"
git commit -m "Create or update GitHub clone count badge"
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "Badge document is already up to date."
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
- name: Push badge document
if: steps.badge.outputs.changed == 'true'
run: git push