name: Propose GeoRelay Data Update on: schedule: - cron: "0 6 * * 0" workflow_dispatch: # Default to read-only. The publishing job receives only the scopes required # to push its branch and publish either a PR or a tracking issue. permissions: contents: read concurrency: group: georelay-data-update cancel-in-progress: false env: SOURCE_REPOSITORY: https://github.com/permissionlesstech/georelays.git UPDATE_BRANCH: automation/georelay-data TRACKING_ISSUE_TITLE: GeoRelay update awaiting pull request jobs: propose-relay-data: name: Validate and propose relay data runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: write pull-requests: write issues: write steps: - name: Checkout reviewed base # Pinned actions/checkout v5 so a mutable action tag cannot change the # code that receives this job's write-capable token. uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd with: ref: main fetch-depth: 0 # Do not expose the write token to fetch/validation subprocesses. persist-credentials: false - name: Test GeoRelay validator run: | set -euo pipefail python3 -m unittest discover -s scripts/tests -p "test_*.py" -v - name: Fetch candidate over pinned HTTPS policy id: upstream run: | set -euo pipefail source_commit=$(git ls-remote --refs "$SOURCE_REPOSITORY" refs/heads/main | awk 'NR == 1 { print $1 }') if [[ ! "$source_commit" =~ ^[0-9a-f]{40}$ ]]; then echo "::error::Could not resolve an immutable upstream commit" exit 1 fi source_url="https://raw.githubusercontent.com/permissionlesstech/georelays/$source_commit/nostr_relays.csv" effective_url=$(curl --fail --show-error --silent --location --proto "=https" --proto-redir "=https" --tlsv1.2 --max-time 60 --retry 3 --retry-all-errors --output "$RUNNER_TEMP/georelays-candidate.csv" --write-out "%{url_effective}" "$source_url") if [[ "$effective_url" != "$source_url" ]]; then echo "::error::Unexpected GeoRelay redirect target: $effective_url" exit 1 fi echo "source_commit=$source_commit" >> "$GITHUB_OUTPUT" echo "source_url=$source_url" >> "$GITHUB_OUTPUT" - name: Validate candidate against reviewed baseline id: validation run: | set -euo pipefail python3 scripts/validate_georelays.py --input "$RUNNER_TEMP/georelays-candidate.csv" --baseline relays/online_relays_gps.csv --output relays/online_relays_gps.csv --github-output "$GITHUB_OUTPUT" - name: Check for a reviewed-file change id: changes run: | set -euo pipefail if git diff --quiet -- relays/online_relays_gps.csv; then echo "changed=false" >> "$GITHUB_OUTPUT" echo "Upstream GeoRelay data already matches main." >> "$GITHUB_STEP_SUMMARY" else echo "changed=true" >> "$GITHUB_OUTPUT" git diff --stat -- relays/online_relays_gps.csv fi - name: Push automation branch and publish review request if: steps.changes.outputs.changed == 'true' env: GH_TOKEN: ${{ github.token }} SOURCE_COMMIT: ${{ steps.upstream.outputs.source_commit }} SOURCE_URL: ${{ steps.upstream.outputs.source_url }} DATA_ROWS: ${{ steps.validation.outputs.data_rows }} UNIQUE_RELAYS: ${{ steps.validation.outputs.unique_relays }} DATA_SHA256: ${{ steps.validation.outputs.sha256 }} run: | set -euo pipefail # Scope credential exposure to this final publishing step. gh auth setup-git git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git switch -C "$UPDATE_BRANCH" git add -- relays/online_relays_gps.csv git diff --cached --quiet && { echo "::error::Expected a staged GeoRelay data change" exit 1 } git commit -m "Update reviewed georelay directory" -m "Upstream-commit: $SOURCE_COMMIT" remote_ref="refs/remotes/origin/$UPDATE_BRANCH" if git fetch --no-tags origin "+refs/heads/$UPDATE_BRANCH:$remote_ref" 2>/dev/null; then remote_sha=$(git rev-parse "$remote_ref") git push --force-with-lease="refs/heads/$UPDATE_BRANCH:$remote_sha" origin "HEAD:refs/heads/$UPDATE_BRANCH" else git push origin "HEAD:refs/heads/$UPDATE_BRANCH" fi body_file="$RUNNER_TEMP/georelay-pr-body.md" { echo "## Automated GeoRelay data proposal" echo echo "- Source: $SOURCE_URL" echo "- Upstream commit: $SOURCE_COMMIT" echo "- Data rows: $DATA_ROWS" echo "- Unique normalized relays: $UNIQUE_RELAYS" echo "- SHA-256: $DATA_SHA256" echo echo "The candidate passed strict UTF-8, schema, size, row-count, secure-host, coordinate, duplicate-conflict, and baseline-delta validation." echo echo "This PR is intentionally not auto-merged. Review the relay additions/removals before merging." } > "$body_file" existing_pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --base main --head "$UPDATE_BRANCH" --json number --jq '.[0].number // empty') pr_error="$RUNNER_TEMP/georelay-pr-error.txt" pr_url="" if [[ -n "$existing_pr" ]]; then if gh pr edit "$existing_pr" --repo "$GITHUB_REPOSITORY" --title "Update reviewed GeoRelay directory" --body-file "$body_file" 2> "$pr_error"; then pr_url=$(gh pr view "$existing_pr" --repo "$GITHUB_REPOSITORY" --json url --jq .url) fi else if created_pr_url=$(gh pr create --repo "$GITHUB_REPOSITORY" --base main --head "$UPDATE_BRANCH" --title "Update reviewed GeoRelay directory" --body-file "$body_file" 2> "$pr_error"); then pr_url="$created_pr_url" fi fi tracking_issue_numbers=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$TRACKING_ISSUE_TITLE\" in:title" --limit 100 --json number,title --jq ".[] | select(.title == \"$TRACKING_ISSUE_TITLE\") | .number") tracking_issues=() if [[ -n "$tracking_issue_numbers" ]]; then mapfile -t tracking_issues <<< "$tracking_issue_numbers" fi if [[ -n "$pr_url" ]]; then for issue_number in "${tracking_issues[@]}"; do gh issue close "$issue_number" --repo "$GITHUB_REPOSITORY" --comment "A pull request is now available at $pr_url; closing this fallback tracking issue." done echo "Published GeoRelay review PR: $pr_url" >> "$GITHUB_STEP_SUMMARY" exit 0 fi echo "::warning::GITHUB_TOKEN could not create or update the GeoRelay pull request; publishing the issues-write fallback." if [[ -s "$pr_error" ]]; then cat "$pr_error" >&2 fi compare_url="https://github.com/${GITHUB_REPOSITORY}/compare/main...${UPDATE_BRANCH}?expand=1" issue_body_file="$RUNNER_TEMP/georelay-tracking-issue-body.md" { echo "## Validated GeoRelay update awaiting review" echo echo "The automation branch was updated, but this workflow token could not create or update the pull request. Use the compare link below to create it manually." echo echo "- Compare and create PR: $compare_url" echo "- Automation branch: $UPDATE_BRANCH" echo "- Source: $SOURCE_URL" echo "- Upstream commit: $SOURCE_COMMIT" echo "- Data rows: $DATA_ROWS" echo "- Unique normalized relays: $UNIQUE_RELAYS" echo "- SHA-256: $DATA_SHA256" echo echo "The snapshot passed the repository's strict validator before the branch was pushed." } > "$issue_body_file" if (( ${#tracking_issues[@]} > 0 )); then primary_issue="${tracking_issues[0]}" gh issue edit "$primary_issue" --repo "$GITHUB_REPOSITORY" --title "$TRACKING_ISSUE_TITLE" --body-file "$issue_body_file" issue_url=$(gh issue view "$primary_issue" --repo "$GITHUB_REPOSITORY" --json url --jq .url) for duplicate_issue in "${tracking_issues[@]:1}"; do gh issue close "$duplicate_issue" --repo "$GITHUB_REPOSITORY" --comment "Closing duplicate GeoRelay automation tracking issue; #$primary_issue is canonical." done else issue_url=$(gh issue create --repo "$GITHUB_REPOSITORY" --title "$TRACKING_ISSUE_TITLE" --body-file "$issue_body_file") fi # Do not claim success until the fallback issue was confirmed. [[ -n "$issue_url" ]] echo "Published GeoRelay tracking issue fallback: $issue_url" >> "$GITHUB_STEP_SUMMARY" - name: Clean obsolete automation review state if: steps.changes.outputs.changed == 'false' env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail gh auth setup-git existing_pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --base main --head "$UPDATE_BRANCH" --json number --jq '.[0].number // empty') if [[ -n "$existing_pr" ]]; then gh pr close "$existing_pr" --repo "$GITHUB_REPOSITORY" --comment "Upstream now matches the reviewed file on main; closing this obsolete automation proposal." echo "Closed obsolete PR #$existing_pr." >> "$GITHUB_STEP_SUMMARY" fi tracking_issue_numbers=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$TRACKING_ISSUE_TITLE\" in:title" --limit 100 --json number,title --jq ".[] | select(.title == \"$TRACKING_ISSUE_TITLE\") | .number") if [[ -n "$tracking_issue_numbers" ]]; then while IFS= read -r issue_number; do gh issue close "$issue_number" --repo "$GITHUB_REPOSITORY" --comment "Upstream now matches the reviewed file on main; closing this obsolete automation tracker." echo "Closed obsolete tracking issue #$issue_number." >> "$GITHUB_STEP_SUMMARY" done <<< "$tracking_issue_numbers" fi if git ls-remote --exit-code --heads origin "refs/heads/$UPDATE_BRANCH" > /dev/null; then git push origin --delete "$UPDATE_BRANCH" echo "Deleted obsolete automation branch $UPDATE_BRANCH." >> "$GITHUB_STEP_SUMMARY" else ls_remote_status=$? if (( ls_remote_status != 2 )); then echo "::error::Could not inspect the obsolete automation branch" exit "$ls_remote_status" fi fi