|
| 1 | +name: Generate Renovate Changelog (Stage 2 - Push) |
| 2 | + |
| 3 | +# Stage 2: runs after Stage 1 completes, in the base-repo context with access to |
| 4 | +# SOLRBOT_GITHUB_TOKEN. Downloads the pre-generated artifact (no fork code executed here), |
| 5 | +# validates metadata, and pushes the changelog file to the fork branch. |
| 6 | +on: |
| 7 | + workflow_run: |
| 8 | + workflows: |
| 9 | + - "Generate Renovate Changelog (Stage 1 - Prepare)" |
| 10 | + types: |
| 11 | + - completed |
| 12 | + |
| 13 | +# Each Stage 2 run corresponds to a unique Stage 1 run (unique workflow_run.id). |
| 14 | +# No cancel-in-progress: Stage 1's concurrency already serializes per-PR. |
| 15 | +concurrency: |
| 16 | + group: renovate-changelog-push-${{ github.event.workflow_run.id }} |
| 17 | + |
| 18 | +permissions: |
| 19 | + actions: read # Required to download artifacts from another workflow run |
| 20 | + contents: read # Minimal; actual write access to fork uses SOLRBOT_GITHUB_TOKEN PAT |
| 21 | + |
| 22 | +jobs: |
| 23 | + push-changelog: |
| 24 | + # Only proceed if Stage 1 succeeded for the expected fork. |
| 25 | + # Checking head_repository here avoids a spurious artifact-not-found failure |
| 26 | + # when Stage 1 ran but skipped its generate job (e.g. non-solrbot PR). |
| 27 | + if: | |
| 28 | + github.event.workflow_run.conclusion == 'success' && |
| 29 | + github.event.workflow_run.head_repository.full_name == 'solrbot/apache-_-solr' |
| 30 | + runs-on: ubuntu-latest |
| 31 | + |
| 32 | + steps: |
| 33 | + - name: Download artifact from Stage 1 |
| 34 | + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 |
| 35 | + with: |
| 36 | + name: renovate-changelog-artifact |
| 37 | + run-id: ${{ github.event.workflow_run.id }} |
| 38 | + github-token: ${{ secrets.GITHUB_TOKEN }} |
| 39 | + path: downloaded-artifact/ |
| 40 | + |
| 41 | + - name: Read and validate PR metadata |
| 42 | + id: meta |
| 43 | + run: | |
| 44 | + set -euo pipefail |
| 45 | + META_FILE="downloaded-artifact/pr-metadata.env" |
| 46 | + if [ ! -f "$META_FILE" ]; then |
| 47 | + echo "::error::Metadata file missing from artifact"; exit 1 |
| 48 | + fi |
| 49 | +
|
| 50 | + # Parse with grep/cut rather than source to avoid executing file content as shell. |
| 51 | + # Use || true so a missing key doesn't abort under set -euo pipefail before the |
| 52 | + # explicit emptiness check below can emit a meaningful error message. |
| 53 | + PR_NUMBER=$(grep '^PR_NUMBER=' "$META_FILE" | cut -d= -f2- || true) |
| 54 | + HEAD_REF=$(grep '^HEAD_REF=' "$META_FILE" | cut -d= -f2- || true) |
| 55 | + HEAD_REPO=$(grep '^HEAD_REPO=' "$META_FILE" | cut -d= -f2- || true) |
| 56 | +
|
| 57 | + if [ -z "$PR_NUMBER" ] || [ -z "$HEAD_REF" ] || [ -z "$HEAD_REPO" ]; then |
| 58 | + echo "::error::Missing required metadata fields (PR_NUMBER, HEAD_REF, or HEAD_REPO)"; exit 1 |
| 59 | + fi |
| 60 | +
|
| 61 | + # Security: verify this is the expected fork before using SOLRBOT_GITHUB_TOKEN |
| 62 | + if [ "$HEAD_REPO" != "solrbot/apache-_-solr" ]; then |
| 63 | + echo "::error::Unexpected HEAD_REPO: '$HEAD_REPO'. Expected 'solrbot/apache-_-solr'. Aborting."; exit 1 |
| 64 | + fi |
| 65 | +
|
| 66 | + # Validate PR_NUMBER is a plain positive integer (prevents injection) |
| 67 | + if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then |
| 68 | + echo "::error::PR_NUMBER is not a valid positive integer: '$PR_NUMBER'"; exit 1 |
| 69 | + fi |
| 70 | +
|
| 71 | + # Validate HEAD_REF using Git's own branch-name rules. This rejects edge cases |
| 72 | + # such as '..', '@{', trailing '.lock', and ':' (dangerous in push refspecs) |
| 73 | + # while still accepting valid Renovate branch names like renovate/node@lts. |
| 74 | + if ! git check-ref-format --branch "$HEAD_REF" > /dev/null 2>&1; then |
| 75 | + echo "::error::HEAD_REF is not a valid Git branch name: '$HEAD_REF'"; exit 1 |
| 76 | + fi |
| 77 | +
|
| 78 | + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" |
| 79 | + echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT" |
| 80 | + echo "head_repo=$HEAD_REPO" >> "$GITHUB_OUTPUT" |
| 81 | + echo "Validated: PR#${PR_NUMBER} on ${HEAD_REPO}@${HEAD_REF}" |
| 82 | +
|
| 83 | + - name: Clone fork branch |
| 84 | + env: |
| 85 | + SOLRBOT_TOKEN: ${{ secrets.SOLRBOT_GITHUB_TOKEN }} |
| 86 | + HEAD_REPO: ${{ steps.meta.outputs.head_repo }} |
| 87 | + HEAD_REF: ${{ steps.meta.outputs.head_ref }} |
| 88 | + run: | |
| 89 | + set -euo pipefail |
| 90 | + # Store credentials so the token never appears in the command line or process list |
| 91 | + git config --global credential.helper store |
| 92 | + printf 'https://x-access-token:%s@github.com\n' "$SOLRBOT_TOKEN" > ~/.git-credentials |
| 93 | + chmod 600 ~/.git-credentials |
| 94 | +
|
| 95 | + git clone --depth=1 --branch "$HEAD_REF" \ |
| 96 | + "https://github.com/${HEAD_REPO}.git" \ |
| 97 | + fork-checkout |
| 98 | +
|
| 99 | + - name: Apply changelog to fork checkout |
| 100 | + id: apply |
| 101 | + env: |
| 102 | + PR_NUMBER: ${{ steps.meta.outputs.pr_number }} |
| 103 | + run: | |
| 104 | + set -euo pipefail |
| 105 | + CHANGELOG_DIR="fork-checkout/changelog/unreleased" |
| 106 | + ARTIFACT_DIR="downloaded-artifact/changelog-unreleased" |
| 107 | +
|
| 108 | + if [ ! -d "$CHANGELOG_DIR" ]; then |
| 109 | + echo "::error::changelog/unreleased not found in fork checkout"; exit 1 |
| 110 | + fi |
| 111 | +
|
| 112 | + if [ ! -d "$ARTIFACT_DIR" ]; then |
| 113 | + echo "::warning::No changelog-unreleased directory in artifact for PR#${PR_NUMBER}" |
| 114 | + echo "has_changes=false" >> "$GITHUB_OUTPUT" |
| 115 | + exit 0 |
| 116 | + fi |
| 117 | +
|
| 118 | + # Remove stale PR#NNN-*.yml files (handles slug changes between synchronize events) |
| 119 | + find "$CHANGELOG_DIR" -maxdepth 1 -name "PR#${PR_NUMBER}-*.yml" -delete -print |
| 120 | +
|
| 121 | + # Copy the new PR#NNN-*.yml file(s) from the artifact |
| 122 | + COPIED=0 |
| 123 | + for f in "${ARTIFACT_DIR}/PR#${PR_NUMBER}-"*.yml; do |
| 124 | + if [ -f "$f" ]; then |
| 125 | + cp -v "$f" "$CHANGELOG_DIR/" |
| 126 | + COPIED=$((COPIED + 1)) |
| 127 | + fi |
| 128 | + done |
| 129 | +
|
| 130 | + if [ "$COPIED" -eq 0 ]; then |
| 131 | + echo "::warning::No PR#${PR_NUMBER}-*.yml file found in artifact" |
| 132 | + echo "has_changes=false" >> "$GITHUB_OUTPUT" |
| 133 | + else |
| 134 | + echo "Copied $COPIED changelog file(s)" |
| 135 | + echo "has_changes=true" >> "$GITHUB_OUTPUT" |
| 136 | + fi |
| 137 | +
|
| 138 | + - name: Commit and push to fork branch |
| 139 | + if: steps.apply.outputs.has_changes == 'true' |
| 140 | + env: |
| 141 | + HEAD_REF: ${{ steps.meta.outputs.head_ref }} |
| 142 | + PR_NUMBER: ${{ steps.meta.outputs.pr_number }} |
| 143 | + run: | |
| 144 | + set -euo pipefail |
| 145 | + cd fork-checkout |
| 146 | +
|
| 147 | + if [ -z "$(git status --porcelain changelog/unreleased/)" ]; then |
| 148 | + echo "No changelog changes (already up to date)" |
| 149 | + exit 0 |
| 150 | + fi |
| 151 | +
|
| 152 | + git config user.name "SolrBot" |
| 153 | + git config user.email "solrbot@cominvent.com" |
| 154 | +
|
| 155 | + git add changelog/unreleased/ |
| 156 | + git commit -m "Add changelog entry for PR#${PR_NUMBER}" |
| 157 | +
|
| 158 | + # Credential store (configured in Clone step) provides authentication |
| 159 | + git push origin "HEAD:refs/heads/${HEAD_REF}" |
| 160 | +
|
| 161 | + # Remove credentials from disk now that the push is complete |
| 162 | + rm -f ~/.git-credentials |
0 commit comments