|
1 | 1 | name: Check Action Versions |
2 | 2 | permissions: |
3 | | - contents: read |
| 3 | + contents: write |
4 | 4 | issues: write |
| 5 | + pull-requests: write |
5 | 6 |
|
6 | 7 | on: |
7 | 8 | schedule: |
|
13 | 14 | runs-on: ubuntu-latest |
14 | 15 | steps: |
15 | 16 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 |
| 17 | + with: |
| 18 | + token: ${{ secrets.ACTION_UPDATER_PAT }} |
16 | 19 |
|
17 | 20 | - name: Check for outdated actions |
18 | 21 | id: check |
@@ -150,6 +153,11 @@ jobs: |
150 | 153 |
|
151 | 154 | echo "Report generated:" |
152 | 155 | cat outdated-actions-report.md |
| 156 | +
|
| 157 | + # Export action data for file modification step |
| 158 | + for action in "${!ACTIONS_SHA[@]}"; do |
| 159 | + echo "${action}|${ACTIONS_CURRENT[$action]}|${ACTIONS_LATEST[$action]}|${ACTIONS_SHA[$action]}" |
| 160 | + done > outdated-actions-data.txt |
153 | 161 | else |
154 | 162 | echo "All actions are up to date!" |
155 | 163 | fi |
@@ -179,18 +187,149 @@ jobs: |
179 | 187 | echo "Created new issue" |
180 | 188 | fi |
181 | 189 |
|
182 | | - - name: Close issue if all actions up to date |
| 190 | + - name: Apply action updates |
| 191 | + if: steps.check.outputs.has_outdated == 'true' |
| 192 | + id: apply |
| 193 | + run: | |
| 194 | + set -euo pipefail |
| 195 | +
|
| 196 | + CHANGES_MADE=0 |
| 197 | +
|
| 198 | + while IFS='|' read -r action current latest sha; do |
| 199 | + echo "Updating $action: $current -> $sha # $latest" |
| 200 | +
|
| 201 | + for workflow in .github/workflows/*.yml; do |
| 202 | + # Escape special characters for sed |
| 203 | + action_escaped=$(echo "$action" | sed 's/[\/&]/\\&/g') |
| 204 | + sha_escaped=$(echo "$sha" | sed 's/[\/&]/\\&/g') |
| 205 | + latest_escaped=$(echo "$latest" | sed 's/[\/&]/\\&/g') |
| 206 | +
|
| 207 | + # Replace: uses: owner/repo@anything -> uses: owner/repo@sha # version |
| 208 | + if grep -q "uses:[[:space:]]*${action}@" "$workflow"; then |
| 209 | + sed -i "s|\(uses:[[:space:]]*${action_escaped}@\)[^[:space:]#]*\(.*\)|\1${sha_escaped} # ${latest_escaped}|" "$workflow" |
| 210 | + CHANGES_MADE=1 |
| 211 | + fi |
| 212 | + done |
| 213 | + done < outdated-actions-data.txt |
| 214 | +
|
| 215 | + echo "changes_made=$CHANGES_MADE" >> "$GITHUB_OUTPUT" |
| 216 | +
|
| 217 | + - name: Configure SSH signing |
| 218 | + if: steps.apply.outputs.changes_made == '1' |
| 219 | + env: |
| 220 | + SSH_SIGNING_KEY: ${{ secrets.SSH_SIGNING_KEY }} |
| 221 | + SSH_PASSPHRASE: ${{ secrets.SSH_SIGNING_KEY_PASSPHRASE }} |
| 222 | + run: | |
| 223 | + mkdir -p ~/.ssh |
| 224 | + printf '%s\n' "$SSH_SIGNING_KEY" > ~/.ssh/signing_key |
| 225 | + chmod 600 ~/.ssh/signing_key |
| 226 | + ssh-keygen -p -P "$SSH_PASSPHRASE" -N "" -f ~/.ssh/signing_key |
| 227 | + git config user.name "nerdalytics" |
| 228 | + git config user.email "97166791+nerdalytics@users.noreply.github.com" |
| 229 | + git config gpg.format ssh |
| 230 | + git config user.signingkey ~/.ssh/signing_key |
| 231 | + git config commit.gpgsign true |
| 232 | +
|
| 233 | + - name: Create branch and commit |
| 234 | + if: steps.apply.outputs.changes_made == '1' |
| 235 | + id: commit |
| 236 | + run: | |
| 237 | + set -euo pipefail |
| 238 | +
|
| 239 | + BRANCH_NAME="automation/update-github-actions" |
| 240 | +
|
| 241 | + git checkout -B "$BRANCH_NAME" |
| 242 | + git add .github/workflows/*.yml |
| 243 | +
|
| 244 | + if git diff --cached --quiet; then |
| 245 | + echo "No changes to commit" |
| 246 | + echo "has_changes=false" >> "$GITHUB_OUTPUT" |
| 247 | + exit 0 |
| 248 | + fi |
| 249 | +
|
| 250 | + OUTDATED_COUNT=$(wc -l < outdated-actions-data.txt) |
| 251 | + git commit -m "chore(deps): update $OUTDATED_COUNT GitHub Action(s) to latest versions |
| 252 | +
|
| 253 | + Updates actions to SHA-pinned versions for security. |
| 254 | + See workflow file changes for details." |
| 255 | +
|
| 256 | + echo "has_changes=true" >> "$GITHUB_OUTPUT" |
| 257 | + echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" |
| 258 | +
|
| 259 | + - name: Push and create PR |
| 260 | + if: steps.commit.outputs.has_changes == 'true' |
| 261 | + env: |
| 262 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 263 | + GH_REPO: ${{ github.repository }} |
| 264 | + run: | |
| 265 | + set -euo pipefail |
| 266 | +
|
| 267 | + BRANCH_NAME="${{ steps.commit.outputs.branch_name }}" |
| 268 | + ISSUE_TITLE="Security: Outdated GitHub Actions detected" |
| 269 | + PR_TITLE="chore(deps): Update GitHub Actions to latest versions" |
| 270 | +
|
| 271 | + # Find issue number for linking |
| 272 | + ISSUE_NUMBER=$(gh issue list --state open --search "in:title $ISSUE_TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "") |
| 273 | +
|
| 274 | + # Generate PR body |
| 275 | + PR_BODY="## Summary |
| 276 | +
|
| 277 | + Automatically updates GitHub Actions to their latest SHA-pinned versions. |
| 278 | +
|
| 279 | + $(cat outdated-actions-report.md) |
| 280 | +
|
| 281 | + --- |
| 282 | + ${ISSUE_NUMBER:+Fixes #$ISSUE_NUMBER} |
| 283 | +
|
| 284 | + *This PR was automatically generated by the [Check Action Versions](.github/workflows/check-action-versions.yml) workflow.*" |
| 285 | +
|
| 286 | + # Close existing PR and delete branch if they exist (avoids force-push) |
| 287 | + EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number' 2>/dev/null || echo "") |
| 288 | + if [[ -n "$EXISTING_PR" ]]; then |
| 289 | + echo "Closing existing PR #$EXISTING_PR to replace with updated version" |
| 290 | + gh pr close "$EXISTING_PR" --comment "Superseded by a new PR with updated changes." --delete-branch || true |
| 291 | + fi |
| 292 | +
|
| 293 | + # Delete remote branch if it still exists (e.g., PR was already closed/merged) |
| 294 | + if git ls-remote --exit-code --heads origin "$BRANCH_NAME" > /dev/null 2>&1; then |
| 295 | + echo "Deleting existing remote branch" |
| 296 | + git push origin --delete "$BRANCH_NAME" || true |
| 297 | + fi |
| 298 | +
|
| 299 | + # Push branch and create new PR |
| 300 | + git push -u origin "$BRANCH_NAME" |
| 301 | +
|
| 302 | + echo "Creating new PR" |
| 303 | + gh pr create \ |
| 304 | + --title "$PR_TITLE" \ |
| 305 | + --body "$PR_BODY" \ |
| 306 | + --label "security,dependencies" \ |
| 307 | + --base main \ |
| 308 | + --head "$BRANCH_NAME" |
| 309 | +
|
| 310 | + - name: Close issue and PR if all actions up to date |
183 | 311 | if: steps.check.outputs.has_outdated == 'false' |
184 | 312 | env: |
185 | 313 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
186 | 314 | GH_REPO: ${{ github.repository }} |
187 | 315 | run: | |
188 | 316 | ISSUE_TITLE="Security: Outdated GitHub Actions detected" |
189 | | - EXISTING_ISSUE=$(gh issue list --state open --search "in:title $ISSUE_TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "") |
| 317 | + BRANCH_NAME="automation/update-github-actions" |
190 | 318 |
|
| 319 | + # Close existing issue |
| 320 | + EXISTING_ISSUE=$(gh issue list --state open --search "in:title $ISSUE_TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "") |
191 | 321 | if [[ -n "$EXISTING_ISSUE" ]]; then |
192 | 322 | gh issue close "$EXISTING_ISSUE" --comment "All GitHub Actions are now up to date." |
193 | 323 | echo "Closed issue #$EXISTING_ISSUE" |
194 | 324 | else |
195 | 325 | echo "No existing issue to close" |
196 | 326 | fi |
| 327 | +
|
| 328 | + # Close existing PR |
| 329 | + EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number' 2>/dev/null || echo "") |
| 330 | + if [[ -n "$EXISTING_PR" ]]; then |
| 331 | + gh pr close "$EXISTING_PR" --comment "All GitHub Actions are now up to date. Closing this automated PR." |
| 332 | + echo "Closed PR #$EXISTING_PR" |
| 333 | + else |
| 334 | + echo "No existing PR to close" |
| 335 | + fi |
0 commit comments