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
14 changes: 8 additions & 6 deletions .github/workflows/pr-preview-cleanup.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
name: PR Preview Cleanup

on:
pull_request:
pull_request_target:
types: [closed]

# Write permissions are safe here — this workflow runs from the base repository
# context and never checks out fork code (it only deletes a directory by PR number).
permissions:
contents: write
pull-requests: write
Expand All @@ -18,7 +20,6 @@ jobs:
cleanup:
name: Remove Preview
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository

steps:
- name: Checkout gh-pages branch
Expand All @@ -39,11 +40,13 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
git rm -rf "${PREVIEW_DIR}"
git commit -m "chore: remove preview for PR #${PR_NUMBER}"

for attempt in 1 2 3; do
git fetch origin gh-pages
git rebase origin/gh-pages 2>/dev/null || { git rebase --abort; true; }
git push origin gh-pages && break
if git rebase origin/gh-pages; then
git push origin gh-pages && break
else
git rebase --abort
fi
echo "Push attempt ${attempt} failed, retrying..."
sleep $((attempt * 5))
done
Expand All @@ -64,7 +67,6 @@ jobs:

const marker = '<!-- pr-preview-bot -->';

// Paginate to handle PRs with more than 30 comments
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: prNumber,
});
Expand Down
153 changes: 153 additions & 0 deletions .github/workflows/pr-preview-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: PR Preview Deploy

on:
workflow_run:
workflows: ["PR Preview"]
types: [completed]

# Write permissions are safe here — this workflow always runs from the base
# repository context (main branch), never from fork code.
permissions:
contents: write
pull-requests: write
Comment on lines +10 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does actions/download-artifact v4 require actions:read permission to download an artifact from a different workflow run via run-id in the same repository?

💡 Result:

Yes, when using actions/download-artifact@v4 to download an artifact from a different workflow run (even within the same repository), you must provide a github-token with the appropriate permissions [1][2][3]. By default, the actions/download-artifact action is scoped to the current workflow run [1][4]. To access artifacts from a different run, you must specify the github-token input [2][3]. This token must have at least actions:read permissions for the target repository to successfully authenticate the request via the GitHub API [5][6]. While the default GITHUB_TOKEN provided in a standard workflow environment is typically scoped to the current repository and workflow, it may lack the necessary permissions to access artifacts from other workflow runs unless specifically configured or replaced with a token (such as a Personal Access Token or a scoped installation token) that possesses the required actions:read access [5]. If the provided token lacks these permissions, the action will fail during the API call [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Workflow metadata:"
if [ -f .github/workflows/pr-preview-deploy.yml ]; then
  sed -n '1,40p' .github/workflows/pr-preview-deploy.yml | cat -n
else
  echo "missing .github/workflows/pr-preview-deploy.yml"
fi

echo
echo "Action download step:"
sed -n '20,36p' .github/workflows/pr-preview-deploy.yml | cat -n 2>/dev/null

echo
echo "Search github-token inputs in download-artifact usages:"
rg -n "download-artifact@v|github-token|run-id" .github/workflows || true

Repository: migtools/mta-documentation

Length of output: 2805


Add actions: read permission for cross-run artifact download.

actions/download-artifact@v4 uses the provided github-token to fetch artifacts from another run via run-id. Since this workflow declares an explicit permission block without actions: read, the deploy job cannot authenticate that artifact request and the preview deployment fails.

🔧 Proposed fix
 permissions:
   contents: write
   pull-requests: write
+  actions: read

Applies to lines: 10-12 and 27-33.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
permissions:
contents: write
pull-requests: write
permissions:
contents: write
pull-requests: write
actions: read
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-preview-deploy.yml around lines 10 - 12, Update the
workflow’s top-level permissions block to include actions: read alongside
contents and pull-requests, so the artifact download step using
actions/download-artifact@v4 and a cross-run run-id can authenticate
successfully.


# Serialize all gh-pages pushes to prevent non-fast-forward failures
# when two PRs deploy at the same time.
concurrency:
group: gh-pages-deploy
cancel-in-progress: false

jobs:
deploy:
name: Deploy Preview
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'

steps:
- name: Download preview artifact
uses: actions/download-artifact@v4
with:
name: pr-preview-site
path: /tmp/pr-preview-site
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: Read PR number from artifact
id: pr
run: |
PR_NUMBER=$(cat /tmp/pr-preview-site/.pr-number)
echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
rm /tmp/pr-preview-site/.pr-number

- name: Get PR head SHA
id: sha
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ steps.pr.outputs.number }},
});
core.setOutput('sha', pr.data.head.sha.substring(0, 7));

- name: Checkout repo
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
persist-credentials: true

- name: Checkout or initialise gh-pages branch
run: |
git fetch origin gh-pages 2>/dev/null || true
if git show-ref --verify --quiet refs/remotes/origin/gh-pages; then
git checkout -B gh-pages origin/gh-pages
else
git checkout --orphan gh-pages
git rm -rf . --quiet
echo "GitHub Pages for mta-documentation" > README.md
git add README.md
git -c user.name="github-actions[bot]" -c user.email="github-actions[bot]@users.noreply.github.com" commit -m "chore: initialise gh-pages branch"
git push origin gh-pages
fi

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Deploy preview to gh-pages
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
COMMIT_SHA: ${{ steps.sha.outputs.sha }}
run: |
PREVIEW_DIR="pr-previews/${PR_NUMBER}"
rm -rf "${PREVIEW_DIR}"
mkdir -p "${PREVIEW_DIR}"
cp -r /tmp/pr-preview-site/. "${PREVIEW_DIR}/"

git add "${PREVIEW_DIR}"
if git diff --staged --quiet; then
echo "No changes to deploy."
else
git commit -m "preview: PR #${PR_NUMBER} @ ${COMMIT_SHA}"
for attempt in 1 2 3; do
git fetch origin gh-pages
if git rebase origin/gh-pages; then
git push origin gh-pages && break
else
git rebase --abort
fi
echo "Push attempt ${attempt} failed, retrying..."
sleep $((attempt * 5))
done
fi

- name: Post or update preview comment
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = ${{ steps.pr.outputs.number }};
const sha = '${{ steps.sha.outputs.sha }}';
const owner = context.repo.owner;
const repo = context.repo.repo;

const previewUrl =
`https://${owner}.github.io/${repo}/pr-previews/${prNumber}/`;

const body = [
`## 📄 Documentation Preview`,
``,
`| | |`,
`|---|---|`,
`| **Preview URL** | ${previewUrl} |`,
`| **Commit** | \`${sha}\` |`,
`| **Updated** | ${new Date().toUTCString()} |`,
``,
`> Preview updates automatically on every push to this PR.`,
`> It will be removed when the PR is closed.`,
].join('\n');

const marker = '<!-- pr-preview-bot -->';
const markedBody = marker + '\n' + body;

const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: prNumber,
});

const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner, repo,
comment_id: existing.id,
body: markedBody,
});
} else {
await github.rest.issues.createComment({
owner, repo,
issue_number: prNumber,
body: markedBody,
});
}
120 changes: 13 additions & 107 deletions .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,20 @@ on:
- 'index.md'
- '.github/workflows/pr-preview.yml'

# Read-only is sufficient — this job only builds and uploads an artifact.
# The deploy job (pr-preview-deploy.yml) runs separately with write permissions
# via workflow_run, which works for fork PRs where this token would be read-only.
permissions:
contents: write
pull-requests: write

# Serialize all gh-pages pushes across both workflows to prevent
# non-fast-forward failures when two PRs deploy at the same time.
concurrency:
group: gh-pages-deploy
cancel-in-progress: false
contents: read

jobs:
build-and-deploy:
name: Build & Deploy Preview
build:
name: Build Preview
runs-on: ubuntu-latest
# Skip fork PRs — GITHUB_TOKEN is read-only for forks and the push would fail.
if: github.event.pull_request.head.repo.full_name == github.repository

steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true

- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand All @@ -58,97 +49,12 @@ jobs:
--baseurl "/mta-documentation/pr-previews/${PR_NUMBER}" \
--destination _site

- name: Deploy preview to gh-pages
env:
PR_NUMBER: ${{ github.event.number }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

mkdir -p /tmp/pr-preview
cp -r _site/. /tmp/pr-preview/

git fetch origin gh-pages 2>/dev/null || true
if git show-ref --verify --quiet refs/remotes/origin/gh-pages; then
git checkout -B gh-pages origin/gh-pages
else
git checkout --orphan gh-pages
git rm -rf . --quiet
echo "GitHub Pages for mta-documentation" > README.md
git add README.md
git commit -m "chore: initialise gh-pages branch"
fi

PREVIEW_DIR="pr-previews/${PR_NUMBER}"
rm -rf "${PREVIEW_DIR}"
mkdir -p "${PREVIEW_DIR}"
cp -r /tmp/pr-preview/. "${PREVIEW_DIR}/"

git add "${PREVIEW_DIR}"
if git diff --staged --quiet; then
echo "No changes to deploy."
else
COMMIT_SHA="${{ github.event.pull_request.head.sha }}"
git commit -m "preview: PR #${PR_NUMBER} @ ${COMMIT_SHA::7}"
- name: Save PR number for deploy workflow
run: echo "${{ github.event.number }}" > _site/.pr-number

# Retry push up to 3 times in case of a concurrent non-fast-forward
for attempt in 1 2 3; do
git fetch origin gh-pages
git rebase origin/gh-pages 2>/dev/null || { git rebase --abort; true; }
git push origin gh-pages && break
echo "Push attempt ${attempt} failed, retrying..."
sleep $((attempt * 5))
done
fi

- name: Post or update preview comment
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ github.event.number }}
- name: Upload preview artifact
uses: actions/upload-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = process.env.PR_NUMBER;
const sha = context.payload.pull_request.head.sha.substring(0, 7);
const owner = context.repo.owner;
const repo = context.repo.repo;

const previewUrl =
`https://${owner}.github.io/${repo}/pr-previews/${prNumber}/`;

const body = [
`## 📄 Documentation Preview`,
``,
`| | |`,
`|---|---|`,
`| **Preview URL** | ${previewUrl} |`,
`| **Commit** | \`${sha}\` |`,
`| **Updated** | ${new Date().toUTCString()} |`,
``,
`> Preview updates automatically on every push to this PR.`,
`> It will be removed when the PR is closed.`,
].join('\n');

const marker = '<!-- pr-preview-bot -->';
const markedBody = marker + '\n' + body;

// Paginate to handle PRs with more than 30 comments
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: prNumber,
});

const existing = comments.find(c => c.body.includes(marker));

if (existing) {
await github.rest.issues.updateComment({
owner, repo,
comment_id: existing.id,
body: markedBody,
});
} else {
await github.rest.issues.createComment({
owner, repo,
issue_number: prNumber,
body: markedBody,
});
}
name: pr-preview-site
path: _site/
retention-days: 1
Loading