feat: add secure repository-owned star history #3
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update Star History | |
| on: | |
| # TEMPORARY: delete this push trigger and the feature-smoke job after the | |
| # feature/monthly-star-history workflow has passed its one-time smoke run. | |
| push: | |
| branches: | |
| - feature/monthly-star-history | |
| schedule: | |
| - cron: '17 3 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| force: | |
| description: Record a snapshot even when 15 days have not elapsed | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: bettafish-star-history | |
| cancel-in-progress: false | |
| jobs: | |
| feature-smoke: | |
| # TEMPORARY: read-only one-shot validation for feature/monthly-star-history. | |
| if: >- | |
| ${{ | |
| github.event_name == 'push' && | |
| github.repository == '666ghj/BettaFish' && | |
| github.ref == 'refs/heads/feature/monthly-star-history' && | |
| github.actor_id == '110395318' && | |
| github.triggering_actor == '666ghj' | |
| }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout exact feature commit without persisted credentials | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ github.sha }} | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Run Star History tests without tokens | |
| env: | |
| GITHUB_TOKEN: '' | |
| GH_TOKEN: '' | |
| run: >- | |
| python3 -m unittest | |
| tests.test_star_history | |
| tests.test_fetch_star_count | |
| -v | |
| - name: Fetch aggregate Star count only | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| output="$RUNNER_TEMP/bettafish-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]+$ ]] | |
| - name: Force record and render offline without tokens | |
| shell: bash | |
| env: | |
| STAR_COUNT_FILE: ${{ runner.temp }}/bettafish-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 smoke outputs without tokens | |
| env: | |
| GITHUB_TOKEN: '' | |
| GH_TOKEN: '' | |
| run: | | |
| python3 scripts/star_history.py check | |
| python3 -m unittest \ | |
| tests.test_star_history \ | |
| tests.test_fetch_star_count \ | |
| -v | |
| due-check: | |
| if: >- | |
| ${{ | |
| github.repository == '666ghj/BettaFish' && | |
| (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') | |
| }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| outputs: | |
| due: ${{ steps.due.outputs.due }} | |
| steps: | |
| - name: Checkout triggering commit | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ github.sha }} | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Check whether a snapshot is due | |
| id: due | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| value="$(python3 scripts/star_history.py due)" | |
| case "$value" in | |
| true|false) ;; | |
| *) | |
| echo "::error::Invalid due result" | |
| exit 1 | |
| ;; | |
| esac | |
| printf 'due=%s\n' "$value" >> "$GITHUB_OUTPUT" | |
| update-main: | |
| needs: due-check | |
| if: >- | |
| ${{ | |
| needs.due-check.result == 'success' && | |
| github.repository == '666ghj/BettaFish' && | |
| github.ref == 'refs/heads/main' && | |
| ( | |
| github.event_name == 'schedule' || | |
| ( | |
| github.event_name == 'workflow_dispatch' && | |
| github.actor_id == '110395318' && | |
| github.triggering_actor == '666ghj' | |
| ) | |
| ) && | |
| ( | |
| needs.due-check.outputs.due == 'true' || | |
| (github.event_name == 'workflow_dispatch' && inputs.force == true) | |
| ) | |
| }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| env: | |
| GIT_TERMINAL_PROMPT: '0' | |
| steps: | |
| - name: Checkout triggering commit | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ github.sha }} | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Run Star History tests without tokens | |
| env: | |
| GITHUB_TOKEN: '' | |
| GH_TOKEN: '' | |
| run: >- | |
| python3 -m unittest | |
| tests.test_star_history | |
| tests.test_fetch_star_count | |
| -v | |
| - name: Fetch aggregate Star count only | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| output="$RUNNER_TEMP/bettafish-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]+$ ]] | |
| - name: Record aggregate Star snapshot offline without tokens | |
| if: ${{ github.event_name != 'workflow_dispatch' || inputs.force != true }} | |
| shell: bash | |
| env: | |
| STAR_COUNT_FILE: ${{ runner.temp }}/bettafish-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" | |
| - name: Force aggregate Star snapshot offline without tokens | |
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.force == true }} | |
| shell: bash | |
| env: | |
| STAR_COUNT_FILE: ${{ runner.temp }}/bettafish-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_star_history \ | |
| tests.test_fetch_star_count \ | |
| -v | |
| - name: Commit exact output allowlist | |
| id: commit | |
| shell: bash | |
| 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 commit \ | |
| -m 'chore: update star history [skip ci]' | |
| printf 'created=true\n' >> "$GITHUB_OUTPUT" | |
| - name: Verify one allowlisted commit and unchanged main target | |
| if: ${{ steps.commit.outputs.created == 'true' }} | |
| shell: bash | |
| 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='refs/heads/main' | |
| [[ "$GITHUB_REPOSITORY" == '666ghj/BettaFish' ]] | |
| [[ "$GITHUB_REF" == "$target_ref" ]] | |
| [[ "$(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/BettaFish|\ | |
| https://github.com/666ghj/BettaFish.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 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 | |
| } | |
| - name: Push one allowlisted commit with an ephemeral credential | |
| if: ${{ steps.commit.outputs.created == 'true' }} | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| GIT_TERMINAL_PROMPT: '0' | |
| GIT_TRACE: '0' | |
| GIT_TRACE_CURL: '0' | |
| GIT_TRACE_PACKET: '0' | |
| GIT_CURL_VERBOSE: '0' | |
| 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" | |
| [[ "$GITHUB_REPOSITORY" == '666ghj/BettaFish' ]] | |
| [[ "$GITHUB_REF" == 'refs/heads/main' ]] | |
| [[ "$(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/BettaFish|\ | |
| https://github.com/666ghj/BettaFish.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 | |
| [[ -n "$GITHUB_TOKEN" ]] | |
| [[ "$GITHUB_TOKEN" != *$'\n'* && "$GITHUB_TOKEN" != *$'\r'* ]] | |
| 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 GITHUB_TOKEN | |
| trap 'unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0' EXIT | |
| git push --porcelain origin HEAD:refs/heads/main |