Auto-update last updated dates

View as Markdown

You can use a GitHub Action to automatically update the last-updated frontmatter property whenever MDX files are modified in a pull request. This keeps your documentation’s “Last updated” timestamp accurate without manual updates.

Set up the workflow

When a pull request is opened or updated, the workflow detects which MDX files changed, updates or adds a last-updated field in their frontmatter with the current date, and commits the changes back to the PR branch. That stamp commit is itself a synchronize event, so the workflow runs a second time on its own commit; the second run stamps the same date, finds no diff, and exits without committing.

1

Create a token

Give the workflow a personal access token to push its stamp commit with. A push authenticated this way triggers the PR’s other workflows the same way a human push does, so fern check and preview deploys run automatically.

  1. Under Settings > Developer settings > Personal access tokens, create a fine-grained token scoped to your docs repository with Contents: Read and write. Classic tokens with the repo scope also work.

  2. Add it under Settings > Secrets and variables > Actions as DOCS_BOT_TOKEN. A token your repository already uses for release or sync automation works too, as does a GitHub App installation token, which avoids per-user expiry on team-maintained repositories.

If your repository runs no other checks on docs PRs, you can skip adding the secret and use the built-in GITHUB_TOKEN instead. GitHub holds every pull_request run the stamp push triggers behind an Approve workflows to run banner until someone with write access approves it, and no repository or organization setting turns this off.

2

Add the workflow

Add this workflow to your documentation repository, with token set to the secret you created. It stamps dates in “Month Day, Year” format (e.g., “December 11, 2025”); change the date command to use a different format.

.github/workflows/update-last-updated.yml
1name: Update last updated date
2
3# Trigger this workflow when PRs are opened or updated
4on:
5 pull_request:
6 types: [opened, synchronize]
7 branches:
8 - main # Adjust to match your main branch name
9
10jobs:
11 update-last-updated:
12 runs-on: ubuntu-latest
13 # No token can push to a contributor's fork, so the push would fail.
14 # Skip fork PRs instead.
15 if: github.event.pull_request.head.repo.full_name == github.repository
16 permissions:
17 contents: write # Required to commit changes
18 pull-requests: write # Required to update the PR
19 steps:
20 # Step 1: Check out the PR branch
21 - name: Checkout repository
22 uses: actions/checkout@v4
23 with:
24 ref: ${{ github.head_ref }} # Check out the PR's source branch
25 fetch-depth: 0
26 token: ${{ secrets.DOCS_BOT_TOKEN }} # A PAT, not GITHUB_TOKEN
27
28 # Step 2: Identify which MDX files changed in this PR
29 - name: Get changed MDX files
30 id: changed-files
31 uses: tj-actions/changed-files@v45
32 with:
33 files: |
34 **/*.mdx # Only track changes to .mdx files
35
36 # Step 3: Update the last-updated field in each changed MDX file
37 - name: Update last-updated frontmatter
38 if: steps.changed-files.outputs.any_changed == 'true'
39 run: |
40 # Generate current date in "Month Day, Year" format (e.g., "December 11, 2025")
41 # Modify the date format here if you prefer a different style
42 CURRENT_DATE=$(date +"%B %-d, %Y")
43 echo "Current date: $CURRENT_DATE"
44
45 # Process each changed MDX file
46 for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
47 echo "Processing: $file"
48
49 # Skip if file was deleted or doesn't exist
50 if [ ! -f "$file" ]; then
51 echo "File not found, skipping: $file"
52 continue
53 fi
54
55 # Check if file has frontmatter (must start with ---)
56 if ! head -1 "$file" | grep -q "^---"; then
57 echo "No frontmatter found, skipping: $file"
58 continue
59 # If file already has a last-updated field, update it
60 elif grep -q "^last-updated:" "$file"; then
61 echo "Updating existing last-updated field"
62 sed -i "s/^last-updated:.*$/last-updated: $CURRENT_DATE/" "$file"
63 # If file has frontmatter but no last-updated field, add it
64 else
65 echo "Adding last-updated field to existing frontmatter"
66 # This awk command inserts the last-updated field just before the closing ---
67 awk -v date="$CURRENT_DATE" '
68 BEGIN { in_frontmatter=0; added=0 }
69 NR==1 && /^---$/ { in_frontmatter=1; print; next }
70 in_frontmatter && /^---$/ && !added { print "last-updated: " date; added=1; print; in_frontmatter=0; next }
71 { print }
72 ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
73 fi
74 done
75
76 # Step 4: Commit and push the updated files back to the PR
77 - name: Commit changes
78 if: steps.changed-files.outputs.any_changed == 'true'
79 run: |
80 git config --local user.email "github-actions[bot]@users.noreply.github.com"
81 git config --local user.name "github-actions[bot]"
82 git add -A
83 # Only commit if there are actual changes
84 if git diff --staged --quiet; then
85 echo "No changes to commit"
86 else
87 git commit -m "chore: update last-updated date in MDX files"
88 git push
89 fi

Skip specific pull requests

Stamping every touched page is wrong for a bulk change: a backfill or a sync PR that touches hundreds of pages would rewrite every date to the merge date. Gate the job on a label to opt those PRs out:

1jobs:
2 update-last-updated:
3 if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-last-updated') && github.event.pull_request.head.repo.full_name == github.repository }}