501 lines
18 KiB
YAML
501 lines
18 KiB
YAML
name: Update Star History
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '17 3 1,16 * *'
|
|
timezone: 'UTC'
|
|
workflow_dispatch:
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: repository-star-history-${{ github.repository_id }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
update-default-branch:
|
|
if: >-
|
|
${{
|
|
github.repository == '666ghj/MiroFish' &&
|
|
github.ref_name == github.event.repository.default_branch &&
|
|
(
|
|
github.event_name == 'schedule' ||
|
|
(
|
|
github.event_name == 'workflow_dispatch' &&
|
|
github.actor_id == '110395318' &&
|
|
github.triggering_actor == '666ghj'
|
|
)
|
|
)
|
|
}}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
env:
|
|
GIT_TERMINAL_PROMPT: '0'
|
|
EXPECTED_REPOSITORY: '666ghj/MiroFish'
|
|
EXPECTED_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
steps:
|
|
- name: Fetch triggering public commit without credentials
|
|
shell: bash
|
|
env:
|
|
GITHUB_TOKEN: ''
|
|
GH_TOKEN: ''
|
|
run: |
|
|
set -euo pipefail
|
|
[[ "${GITHUB_REPOSITORY,,}" == "${EXPECTED_REPOSITORY,,}" ]]
|
|
[[ "$GITHUB_REF" == "refs/heads/$EXPECTED_DEFAULT_BRANCH" ]]
|
|
git init .
|
|
git remote add origin 'https://github.com/666ghj/MiroFish.git'
|
|
git \
|
|
-c credential.helper= \
|
|
-c http.followRedirects=false \
|
|
fetch \
|
|
--no-tags \
|
|
--depth=1 \
|
|
origin \
|
|
"$GITHUB_REF"
|
|
[[ "$(git rev-parse FETCH_HEAD)" == "$GITHUB_SHA" ]]
|
|
git -c core.hooksPath=/dev/null checkout --detach "$GITHUB_SHA"
|
|
[[ -z "$(git status --porcelain --untracked-files=all)" ]]
|
|
|
|
- name: Run Star History tests without tokens
|
|
env:
|
|
GITHUB_TOKEN: ''
|
|
GH_TOKEN: ''
|
|
run: >-
|
|
python3 -m unittest
|
|
tests.test_local_star_history
|
|
tests.test_local_star_count_fetch
|
|
-v
|
|
|
|
- name: Fetch aggregate Star count only
|
|
id: count
|
|
shell: bash
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
umask 077
|
|
printf '%s %s\n' \
|
|
'dfe9e0060d9abb0b3e1cda61bd73bba77fe878815adcbea14601666dce30e927' \
|
|
'scripts/fetch_star_count.py' |
|
|
sha256sum --check --strict -
|
|
output="$RUNNER_TEMP/repository-star-count.txt"
|
|
[[ ! -e "$output" && ! -L "$output" ]]
|
|
python3 scripts/fetch_star_count.py > "$output"
|
|
[[ -f "$output" && ! -L "$output" ]]
|
|
(( $(wc -c < "$output") <= 32 ))
|
|
mapfile -t lines < "$output"
|
|
(( ${#lines[@]} == 1 ))
|
|
[[ "${lines[0]}" =~ ^[0-9]+$ ]]
|
|
printf 'value=%s\n' "${lines[0]}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Record scheduled aggregate Star snapshot offline without tokens
|
|
shell: bash
|
|
env:
|
|
STAR_COUNT_FILE: ${{ runner.temp }}/repository-star-count.txt
|
|
GITHUB_TOKEN: ''
|
|
GH_TOKEN: ''
|
|
run: |
|
|
set -euo pipefail
|
|
trap 'rm -f -- "$STAR_COUNT_FILE"' EXIT
|
|
[[ -z "${GITHUB_TOKEN:-}" && -z "${GH_TOKEN:-}" ]]
|
|
python3 scripts/star_history.py record \
|
|
--count-file "$STAR_COUNT_FILE" \
|
|
--force
|
|
|
|
- name: Verify generated outputs without tokens
|
|
env:
|
|
GITHUB_TOKEN: ''
|
|
GH_TOKEN: ''
|
|
run: |
|
|
python3 scripts/star_history.py check
|
|
python3 -m unittest \
|
|
tests.test_local_star_history \
|
|
tests.test_local_star_count_fetch \
|
|
-v
|
|
|
|
- name: Commit exact output allowlist
|
|
id: commit
|
|
shell: bash
|
|
env:
|
|
GITHUB_TOKEN: ''
|
|
GH_TOKEN: ''
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
allowed() {
|
|
case "$1" in
|
|
.github/star-history/history.json|\
|
|
static/image/star-history-light.svg|\
|
|
static/image/star-history-dark.svg) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
bad=0
|
|
while IFS= read -r -d '' path; do
|
|
if ! allowed "$path"; then
|
|
printf '::error::Unexpected changed path: %q\n' "$path"
|
|
bad=1
|
|
fi
|
|
done < <(
|
|
git diff --name-only -z
|
|
git diff --cached --name-only -z
|
|
git ls-files --others --exclude-standard -z
|
|
)
|
|
(( bad == 0 )) || exit 1
|
|
|
|
for path in \
|
|
.github/star-history/history.json \
|
|
static/image/star-history-light.svg \
|
|
static/image/star-history-dark.svg
|
|
do
|
|
[[ -f "$path" && ! -L "$path" && -s "$path" ]] || {
|
|
printf '::error::Invalid output file: %s\n' "$path"
|
|
exit 1
|
|
}
|
|
[[ "$(realpath -e -- "$path")" == "$GITHUB_WORKSPACE/$path" ]] || {
|
|
printf '::error::Output escaped workspace: %s\n' "$path"
|
|
exit 1
|
|
}
|
|
done
|
|
|
|
git add -- \
|
|
.github/star-history/history.json \
|
|
static/image/star-history-light.svg \
|
|
static/image/star-history-dark.svg
|
|
|
|
if git diff --cached --quiet; then
|
|
printf 'created=false\n' >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
count=0
|
|
while IFS= read -r -d '' path; do
|
|
allowed "$path" || exit 1
|
|
((count += 1))
|
|
done < <(git diff --cached --name-only -z)
|
|
(( count > 0 )) || exit 1
|
|
|
|
git config user.name 'github-actions[bot]'
|
|
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
|
git \
|
|
-c commit.gpgsign=false \
|
|
-c core.hooksPath=/dev/null \
|
|
commit \
|
|
-m 'chore: update star history [skip ci]'
|
|
printf 'created=true\n' >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Verify one allowlisted commit and unchanged main target
|
|
id: verify
|
|
if: ${{ steps.commit.outputs.created == 'true' }}
|
|
shell: bash
|
|
env:
|
|
GITHUB_TOKEN: ''
|
|
GH_TOKEN: ''
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
allowed() {
|
|
case "$1" in
|
|
.github/star-history/history.json|\
|
|
static/image/star-history-light.svg|\
|
|
static/image/star-history-dark.svg) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
base="$GITHUB_SHA"
|
|
target_ref="$GITHUB_REF"
|
|
[[ "$GITHUB_RUN_ID" =~ ^[0-9]+$ ]]
|
|
[[ "$GITHUB_RUN_ATTEMPT" =~ ^[0-9]+$ ]]
|
|
update_branch="automation/star-history/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
|
[[ "${GITHUB_REPOSITORY,,}" == "${EXPECTED_REPOSITORY,,}" ]]
|
|
[[ "$GITHUB_REF" == "refs/heads/$EXPECTED_DEFAULT_BRANCH" ]]
|
|
[[ "$(git rev-parse HEAD^)" == "$base" ]]
|
|
[[ "$(git rev-list --count "${base}..HEAD")" == 1 ]]
|
|
[[ -z "$(git status --porcelain --untracked-files=all)" ]]
|
|
|
|
origin="$(git remote get-url origin)"
|
|
case "$origin" in
|
|
https://github.com/666ghj/MiroFish|\
|
|
https://github.com/666ghj/MiroFish.git) ;;
|
|
*)
|
|
echo "::error::Unexpected origin"
|
|
exit 1
|
|
;;
|
|
esac
|
|
mapfile -t push_urls < <(git remote get-url --push --all origin)
|
|
(( ${#push_urls[@]} == 1 ))
|
|
[[ "${push_urls[0]}" == "$origin" ]]
|
|
|
|
count=0
|
|
while IFS= read -r -d '' path; do
|
|
allowed "$path" || {
|
|
printf '::error::Unexpected committed path: %q\n' "$path"
|
|
exit 1
|
|
}
|
|
((count += 1))
|
|
done < <(git diff-tree --no-commit-id --name-only -r -z HEAD)
|
|
(( count > 0 )) || exit 1
|
|
|
|
git \
|
|
-c credential.helper= \
|
|
-c http.followRedirects=false \
|
|
fetch \
|
|
--no-tags \
|
|
--depth=1 \
|
|
origin \
|
|
"$target_ref"
|
|
[[ "$(git rev-parse FETCH_HEAD)" == "$base" ]] || {
|
|
echo "::error::Target advanced; refusing to rebase or overwrite"
|
|
exit 1
|
|
}
|
|
|
|
if git \
|
|
-c credential.helper= \
|
|
-c http.followRedirects=false \
|
|
ls-remote \
|
|
--exit-code \
|
|
--heads \
|
|
origin \
|
|
"refs/heads/$update_branch" >/dev/null
|
|
then
|
|
echo "::error::Temporary branch already exists"
|
|
exit 1
|
|
else
|
|
status=$?
|
|
[[ "$status" -eq 2 ]] || exit "$status"
|
|
fi
|
|
|
|
printf 'branch=%s\n' "$update_branch" >> "$GITHUB_OUTPUT"
|
|
printf 'head=%s\n' "$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Publish through a verified pull request with an ephemeral credential
|
|
id: publish
|
|
if: ${{ steps.commit.outputs.created == 'true' }}
|
|
shell: bash
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
UPDATE_BRANCH: ${{ steps.verify.outputs.branch }}
|
|
EXPECTED_HEAD: ${{ steps.verify.outputs.head }}
|
|
EXPECTED_STAR_COUNT: ${{ steps.count.outputs.value }}
|
|
GIT_TERMINAL_PROMPT: '0'
|
|
run: |
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
allowed() {
|
|
case "$1" in
|
|
.github/star-history/history.json|\
|
|
static/image/star-history-light.svg|\
|
|
static/image/star-history-dark.svg) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
base="$GITHUB_SHA"
|
|
head="$(git rev-parse HEAD)"
|
|
update_branch="$UPDATE_BRANCH"
|
|
[[ "$GITHUB_RUN_ID" =~ ^[0-9]+$ ]]
|
|
[[ "$GITHUB_RUN_ATTEMPT" =~ ^[0-9]+$ ]]
|
|
[[ "$update_branch" == "automation/star-history/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" ]]
|
|
[[ "$head" == "$EXPECTED_HEAD" ]]
|
|
[[ "$EXPECTED_STAR_COUNT" =~ ^[0-9]+$ ]]
|
|
[[ "${GITHUB_REPOSITORY,,}" == "${EXPECTED_REPOSITORY,,}" ]]
|
|
[[ "$GITHUB_REF" == "refs/heads/$EXPECTED_DEFAULT_BRANCH" ]]
|
|
[[ "$(git rev-parse HEAD^)" == "$base" ]]
|
|
[[ "$(git rev-list --count "${base}..HEAD")" == 1 ]]
|
|
[[ -z "$(git status --porcelain --untracked-files=all)" ]]
|
|
|
|
origin="$(git remote get-url origin)"
|
|
case "$origin" in
|
|
https://github.com/666ghj/MiroFish|\
|
|
https://github.com/666ghj/MiroFish.git) ;;
|
|
*)
|
|
echo "::error::Unexpected origin"
|
|
exit 1
|
|
;;
|
|
esac
|
|
mapfile -t push_urls < <(git remote get-url --push --all origin)
|
|
(( ${#push_urls[@]} == 1 ))
|
|
[[ "${push_urls[0]}" == "$origin" ]]
|
|
|
|
count=0
|
|
while IFS= read -r -d '' path; do
|
|
allowed "$path" || {
|
|
printf '::error::Unexpected committed path: %q\n' "$path"
|
|
exit 1
|
|
}
|
|
((count += 1))
|
|
done < <(git diff-tree --no-commit-id --name-only -r -z HEAD)
|
|
(( count > 0 )) || exit 1
|
|
|
|
expected_files="$RUNNER_TEMP/star-history-expected-files-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.txt"
|
|
pr_files="$RUNNER_TEMP/star-history-pr-files-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.txt"
|
|
open_prs="$RUNNER_TEMP/star-history-open-prs-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.txt"
|
|
body_file="$RUNNER_TEMP/star-history-pr-body-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.md"
|
|
[[ ! -e "$expected_files" && ! -L "$expected_files" ]]
|
|
[[ ! -e "$pr_files" && ! -L "$pr_files" ]]
|
|
[[ ! -e "$open_prs" && ! -L "$open_prs" ]]
|
|
[[ ! -e "$body_file" && ! -L "$body_file" ]]
|
|
git diff-tree --no-commit-id --name-only -r HEAD |
|
|
LC_ALL=C sort > "$expected_files"
|
|
[[ -s "$expected_files" && ! -L "$expected_files" ]]
|
|
trap '
|
|
rm -f -- "$expected_files" "$pr_files" "$open_prs" "$body_file"
|
|
unset GH_TOKEN GITHUB_TOKEN
|
|
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
|
|
' EXIT
|
|
|
|
[[ -n "$GITHUB_TOKEN" ]]
|
|
[[ "$GITHUB_TOKEN" != *$'\n'* && "$GITHUB_TOKEN" != *$'\r'* ]]
|
|
command -v gh >/dev/null
|
|
export GH_TOKEN="$GITHUB_TOKEN"
|
|
|
|
gh api --paginate \
|
|
"repos/$EXPECTED_REPOSITORY/pulls?state=open&base=$EXPECTED_DEFAULT_BRANCH&per_page=100" \
|
|
--jq '.[] | [.number, .user.login, .head.repo.full_name, .head.ref] | @tsv' \
|
|
> "$open_prs"
|
|
[[ -f "$open_prs" && ! -L "$open_prs" ]]
|
|
while IFS=$'\t' read -r existing_number existing_author existing_repo existing_head; do
|
|
if [[ \
|
|
"$existing_number" =~ ^[0-9]+$ &&
|
|
"$existing_author" == 'github-actions[bot]' &&
|
|
"${existing_repo,,}" == "${EXPECTED_REPOSITORY,,}" &&
|
|
"$existing_head" == automation/star-history/*
|
|
]]; then
|
|
echo "::error::An existing automated Star History pull request is still open: #$existing_number"
|
|
exit 1
|
|
fi
|
|
done < "$open_prs"
|
|
|
|
current_base="$(
|
|
gh api \
|
|
"repos/$EXPECTED_REPOSITORY/git/ref/heads/$EXPECTED_DEFAULT_BRANCH" \
|
|
--jq '.object.sha'
|
|
)"
|
|
[[ "$current_base" == "$base" ]]
|
|
|
|
encoded="$(
|
|
printf 'x-access-token:%s' "$GITHUB_TOKEN" |
|
|
base64 |
|
|
tr -d '\n'
|
|
)"
|
|
export GIT_CONFIG_COUNT=1
|
|
export GIT_CONFIG_KEY_0="http.${origin}.extraheader"
|
|
export GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $encoded"
|
|
unset encoded
|
|
|
|
git \
|
|
-c core.hooksPath=/dev/null \
|
|
-c credential.helper= \
|
|
push \
|
|
--porcelain \
|
|
origin \
|
|
"HEAD:refs/heads/$update_branch"
|
|
|
|
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
|
|
|
|
remote_head="$(
|
|
gh api \
|
|
"repos/$EXPECTED_REPOSITORY/git/ref/heads/$update_branch" \
|
|
--jq '.object.sha'
|
|
)"
|
|
[[ "$remote_head" == "$head" ]]
|
|
|
|
current_base="$(
|
|
gh api \
|
|
"repos/$EXPECTED_REPOSITORY/git/ref/heads/$EXPECTED_DEFAULT_BRANCH" \
|
|
--jq '.object.sha'
|
|
)"
|
|
[[ "$current_base" == "$base" ]]
|
|
|
|
{
|
|
echo 'Automated aggregate Star History refresh.'
|
|
echo
|
|
echo 'Please review and merge this pull request manually.'
|
|
echo
|
|
printf -- '- Triggering main SHA: `%s`\n' "$base"
|
|
printf -- '- Generated commit SHA: `%s`\n' "$head"
|
|
printf -- '- Recorded Star count: `%s`\n' "$EXPECTED_STAR_COUNT"
|
|
echo '- Allowed files: history JSON plus light and dark SVGs only.'
|
|
} > "$body_file"
|
|
[[ -s "$body_file" && ! -L "$body_file" ]]
|
|
|
|
create_result="$(
|
|
gh api --method POST "repos/$EXPECTED_REPOSITORY/pulls" \
|
|
-f 'title=chore: update Star History' \
|
|
-f "head=$update_branch" \
|
|
-f "base=$EXPECTED_DEFAULT_BRANCH" \
|
|
-f "body=$(<"$body_file")" \
|
|
--jq '[.number, .html_url] | @tsv'
|
|
)"
|
|
IFS=$'\t' read -r pr_number pr_url <<< "$create_result"
|
|
[[ "$pr_number" =~ ^[0-9]+$ ]]
|
|
[[ "$pr_url" == "https://github.com/$EXPECTED_REPOSITORY/pull/$pr_number" ]]
|
|
|
|
validate_open_pr() {
|
|
local metadata
|
|
local pr_state pr_draft pr_base_repo pr_base_ref pr_base_sha
|
|
local pr_head_repo pr_head_ref pr_head_sha pr_author
|
|
|
|
metadata="$(
|
|
gh api "repos/$EXPECTED_REPOSITORY/pulls/$pr_number" \
|
|
--jq '[.state, (.draft | tostring), .base.repo.full_name, .base.ref, .base.sha, .head.repo.full_name, .head.ref, .head.sha, .user.login] | @tsv'
|
|
)"
|
|
IFS=$'\t' read -r \
|
|
pr_state pr_draft pr_base_repo pr_base_ref pr_base_sha \
|
|
pr_head_repo pr_head_ref pr_head_sha pr_author <<< "$metadata"
|
|
[[ "$pr_state" == "open" ]]
|
|
[[ "$pr_draft" == "false" ]]
|
|
[[ "${pr_base_repo,,}" == "${EXPECTED_REPOSITORY,,}" ]]
|
|
[[ "$pr_base_ref" == "$EXPECTED_DEFAULT_BRANCH" ]]
|
|
[[ "$pr_base_sha" == "$base" ]]
|
|
[[ "${pr_head_repo,,}" == "${EXPECTED_REPOSITORY,,}" ]]
|
|
[[ "$pr_head_ref" == "$update_branch" ]]
|
|
[[ "$pr_head_sha" == "$head" ]]
|
|
[[ "$pr_author" == 'github-actions[bot]' ]]
|
|
}
|
|
|
|
validate_open_pr
|
|
gh api \
|
|
"repos/$EXPECTED_REPOSITORY/pulls/$pr_number/files?per_page=100" \
|
|
--jq '.[].filename' |
|
|
LC_ALL=C sort > "$pr_files"
|
|
[[ -s "$pr_files" && ! -L "$pr_files" ]]
|
|
cmp --silent "$expected_files" "$pr_files"
|
|
|
|
validate_open_pr
|
|
remote_head="$(
|
|
gh api \
|
|
"repos/$EXPECTED_REPOSITORY/git/ref/heads/$update_branch" \
|
|
--jq '.object.sha'
|
|
)"
|
|
[[ "$remote_head" == "$head" ]]
|
|
|
|
current_base="$(
|
|
gh api \
|
|
"repos/$EXPECTED_REPOSITORY/git/ref/heads/$EXPECTED_DEFAULT_BRANCH" \
|
|
--jq '.object.sha'
|
|
)"
|
|
[[ "$current_base" == "$base" ]]
|
|
|
|
{
|
|
echo '### Star History pull request'
|
|
echo
|
|
printf -- '- Pull request: [#%s](%s)\n' "$pr_number" "$pr_url"
|
|
printf -- '- Base SHA: `%s`\n' "$base"
|
|
printf -- '- Head SHA: `%s`\n' "$head"
|
|
printf -- '- Recorded Star count: `%s`\n' "$EXPECTED_STAR_COUNT"
|
|
echo '- Merge: manual owner review required'
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
printf 'branch=%s\n' "$update_branch" >> "$GITHUB_OUTPUT"
|
|
printf 'head_sha=%s\n' "$head" >> "$GITHUB_OUTPUT"
|
|
printf 'pr_number=%s\n' "$pr_number" >> "$GITHUB_OUTPUT"
|
|
printf 'pr_url=%s\n' "$pr_url" >> "$GITHUB_OUTPUT"
|