Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/scripts/sticky-pr-comment.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Post or update a "sticky" PR comment, identified by an HTML marker comment
# in the first line of the body. Avoids spamming a long-running PR with a new
# comment on every push.
#
# Usage: sticky-pr-comment.sh <owner/repo> <pr-number> <body-file>
#
# The body file's FIRST line MUST be the HTML marker (e.g.
# <!-- regen-dockerfiles-bot -->
# ). Subsequent runs find the prior comment by exact-prefix match on that
# marker and PATCH it in place; if none exists yet, a new comment is created.
#
# Requires: gh (authenticated, with pull-requests:write), jq.

set -euo pipefail

repo=$1
pr=$2
body_file=$3

if [ ! -s "$body_file" ]; then
echo "sticky-pr-comment: body file '$body_file' is empty or missing" >&2
exit 1
fi

marker=$(head -n 1 "$body_file")
case "$marker" in
'<!--'*'-->') ;;
*)
echo "sticky-pr-comment: first line of body file must be an HTML marker comment, got: $marker" >&2
exit 1
;;
esac

existing_id=$(gh api "repos/${repo}/issues/${pr}/comments" --paginate \
--jq ".[] | select(.body | startswith(\"${marker}\")) | .id" \
| head -n 1)

if [ -n "$existing_id" ]; then
echo "sticky-pr-comment: updating existing comment $existing_id"
jq -n --rawfile body "$body_file" '{body: $body}' \
| gh api -X PATCH "repos/${repo}/issues/comments/${existing_id}" --input -
else
echo "sticky-pr-comment: creating new comment"
gh pr comment "$pr" --repo "$repo" --body-file "$body_file"
fi
121 changes: 121 additions & 0 deletions .github/workflows/regen-dockerfiles-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: Regenerate Dockerfiles (fork comment)

# For fork PRs: pull_request_target runs in base context with pull-requests
# write, so we can comment. Head code is never executed — we run the base
# branch's trusted generator against the head's snippet data, and refuse
# entirely if the generator script itself was modified in the PR.

on:
pull_request_target:
types: [opened, synchronize, reopened]
paths:
- "docker/snippets/**"
- "docker/images.json"
- "docker/generate-dockerfiles.ps1"

permissions: {}

concurrency:
group: regen-dockerfiles-comment-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
comment:
if: github.event.pull_request.head.repo.full_name != github.repository
name: Diff and comment
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout base (trusted generator)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Checkout PR head into ./pr (data only, never executed)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event.pull_request.head.sha }}
path: pr
persist-credentials: false

- name: Detect generator-script modification
id: gen
run: |
if ! diff -q docker/generate-dockerfiles.ps1 pr/docker/generate-dockerfiles.ps1 >/dev/null 2>&1; then
echo "generator_changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Use PR's data inputs with base's generator
if: steps.gen.outputs.generator_changed != 'true'
run: |
rsync -a --delete pr/docker/snippets/ docker/snippets/
cp pr/docker/images.json docker/images.json

- name: Run trusted generator
if: steps.gen.outputs.generator_changed != 'true'
run: pwsh docker/generate-dockerfiles.ps1

- name: Detect drift
if: steps.gen.outputs.generator_changed != 'true'
id: drift
run: |
# git diff alone misses untracked files — a new image in images.json
# produces a brand-new Dockerfile.* that's untracked, not "modified".
# git status --porcelain covers modified, deleted, AND untracked in
# one shot.
if [ -z "$(git status --porcelain -- docker/generated)" ]; then
echo "drift=false" >> "$GITHUB_OUTPUT"
else
echo "drift=true" >> "$GITHUB_OUTPUT"
git diff --stat docker/generated > /tmp/diffstat.txt
untracked=$(git ls-files --others --exclude-standard -- docker/generated)
if [ -n "$untracked" ]; then
echo "" >> /tmp/diffstat.txt
echo "Untracked new generated files:" >> /tmp/diffstat.txt
echo "$untracked" >> /tmp/diffstat.txt
fi
fi

- name: Comment (drift)
if: steps.drift.outputs.drift == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
{
echo "<!-- regen-dockerfiles-bot -->"
echo "Generated Dockerfiles in this PR are out of date with the snippet config."
echo ""
echo "Please run:"
echo ""
echo '```'
echo "pwsh docker/generate-dockerfiles.ps1"
echo '```'
echo ""
echo "and commit \`docker/generated/\` to your branch."
echo ""
echo "<details><summary>What's stale</summary>"
echo ""
echo '```'
cat /tmp/diffstat.txt
echo '```'
echo ""
echo "</details>"
} > /tmp/body.md
"${GITHUB_WORKSPACE}/.github/scripts/sticky-pr-comment.sh" \
"${{ github.repository }}" \
"${{ github.event.pull_request.number }}" \
/tmp/body.md

- name: Comment (generator changed)
if: steps.gen.outputs.generator_changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
{
echo "<!-- regen-dockerfiles-bot -->"
echo "This PR modifies \`docker/generate-dockerfiles.ps1\`. Auto-regeneration is skipped for safety — please run \`pwsh docker/generate-dockerfiles.ps1\` locally and commit any changes under \`docker/generated/\`."
} > /tmp/body.md
"${GITHUB_WORKSPACE}/.github/scripts/sticky-pr-comment.sh" \
"${{ github.repository }}" \
"${{ github.event.pull_request.number }}" \
/tmp/body.md
52 changes: 52 additions & 0 deletions .github/workflows/regen-dockerfiles-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Regenerate Dockerfiles (push)

# Auto-regenerates docker/generated/* when a same-repo PR changes snippet
# inputs or the generator script, and pushes the result back to the PR
# branch. Fork PRs are handled by regen-dockerfiles-comment.yml because
# pull_request's GITHUB_TOKEN is read-only for forks.

on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "docker/snippets/**"
- "docker/images.json"
- "docker/generate-dockerfiles.ps1"

permissions: {}

concurrency:
group: regen-dockerfiles-push-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
push:
if: github.event.pull_request.head.repo.full_name == github.repository
name: Regenerate and push
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: Regenerate
run: pwsh docker/generate-dockerfiles.ps1

- name: Commit and push if drift
run: |
# git diff alone misses untracked files — if images.json adds a new
# image, the generator creates a brand-new Dockerfile.* that doesn't
# yet exist in the index. git status --porcelain catches modified,
# deleted, AND untracked entries in one shot.
if [ -z "$(git status --porcelain -- docker/generated)" ]; then
echo "No drift."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add docker/generated
git commit -m "chore(docker): regenerate Dockerfiles after snippet/config change"
git push origin HEAD:${{ github.event.pull_request.head.ref }}
Loading