Reusable GitHub Actions workflows for managing forked third-party GitHub Actions with automated upstream sync, tag monitoring, and security scanning.
When you fork a third-party GitHub Action to pin it for supply chain security, you need to keep it in sync with upstream while reviewing changes before they reach your workflows. These reusable workflows automate that process.
Each fork contains thin caller workflows (~20 lines) that reference these reusable workflows via workflow_call. Centralized maintenance means bug fixes and detection improvements propagate to all forks automatically via the floating @v1 tag.
Forks are bootstrapped by fork-action.sh, which creates the caller workflows, branch structure, and manifest.
Syncs the fork's upstream-tracking branch with the upstream repository's default branch.
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
upstream_owner |
string | yes | Owner of the upstream repository |
upstream_repo |
string | yes | Name of the upstream repository |
default_branch |
string | yes | Default branch of the upstream repository |
force |
boolean | no | Force sync even if already up to date (default: false) |
What it does:
- Checks out the fork's
upstream-trackingbranch - Verifies upstream repo identity against
repo_idstored inFORK_MANIFEST.json(detects name squatting / repo transfer attacks) - Fetches the upstream default branch and attempts a fast-forward merge
- Validates and updates
last_synced_shain the manifest (enforces 40-char hex format) - Opens an issue if divergence is detected (non-fast-forward)
- Creates a PR from
upstream-trackingto the fork's default branch with diff stats, security-relevant file list, and review checklist - Adds
security-alertlabel when security-relevant files are changed (action.yml, Dockerfile, package.json, shell scripts, dist/)
Outputs:
| Output | Description |
|---|---|
pr_created |
Whether a sync PR was created or already exists |
pr_number |
PR number |
in_sync |
Whether the fork is already up to date |
merge_success |
Whether fast-forward merge succeeded |
has_security_changes |
Whether security-relevant files were changed |
Monitors upstream tags for new releases, mutations (supply chain risk), and deletions.
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
upstream_owner |
string | yes | Owner of the upstream repository |
upstream_repo |
string | yes | Name of the upstream repository |
What it does:
- Compares upstream and fork tags using dereferenced commit SHAs (handles annotated vs lightweight tag differences)
- Sanitizes tag names via allowlist (
a-zA-Z0-9._+-) to prevent injection - Creates issues for new upstream releases with security review checklists
- Creates security alerts for tag mutations (upstream tag now points to different commit)
- Detects drift between fork tags and
pinned_tagsinFORK_MANIFEST.json - Creates notices for tag deletions (upstream removed a tag)
Runs security analysis on sync PRs, triggered via workflow_run after Sync Upstream completes.
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
default_branch |
string | yes | Default branch of the fork |
What it does:
- Dependency review (fails on high severity)
- CodeQL analysis (JavaScript/TypeScript repos only; informational, excluded from aggregate status)
- Composite action analysis -- detects changes to
action.ymlrefs,usingfield modifications, unpinned action references, and modifiedrunsteps; creates security alert issues for significant findings - Diff summary with risk assessment posted as PR comment
sync-upstream.yml (in your fork):
name: Sync Upstream
on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
inputs:
force:
description: 'Force sync even if already up to date'
required: false
type: boolean
default: false
permissions:
contents: write
pull-requests: write
issues: write
concurrency:
group: sync-upstream
cancel-in-progress: false
jobs:
sync:
uses: tyler-technologies-oss/external-action-shared-workflows/.github/workflows/sync-upstream.yml@v1
with:
upstream_owner: original-owner
upstream_repo: original-repo
default_branch: main
force: ${{ inputs.force == true }}sync-tags.yml (in your fork):
name: Sync Tags
on:
schedule:
- cron: '0 6 * * 3'
workflow_dispatch:
permissions:
contents: read
issues: write
concurrency:
group: sync-tags
cancel-in-progress: false
jobs:
check-tags:
uses: tyler-technologies-oss/external-action-shared-workflows/.github/workflows/sync-tags.yml@v1
with:
upstream_owner: original-owner
upstream_repo: original-reposecurity-scan.yml (in your fork):
name: Security Scan
on:
workflow_run:
workflows: ["Sync Upstream"]
types: [completed]
permissions:
contents: read
pull-requests: write
statuses: write
checks: write
issues: write
security-events: write
concurrency:
group: security-scan
cancel-in-progress: false
jobs:
scan:
if: github.event.workflow_run.conclusion == 'success'
uses: tyler-technologies-oss/external-action-shared-workflows/.github/workflows/security-scan.yml@v1
with:
default_branch: mainThis repo uses semantic versioning with a floating major version tag:
| Change Type | Example | Version Bump | Fork Action |
|---|---|---|---|
| Bug fix | Fix SHA comparison | v1.x.y patch |
None -- auto-propagates via @v1 |
| New optional input | Add dry_run |
v1.x.0 minor |
None -- auto-propagates |
| Remove/rename input | Rename force |
v2.0.0 major |
Update callers, re-run fork-action.sh |
git tag -a v1.1.0 -m "Improved tag mutation detection"
git tag -f v1 -m "Latest v1.x release"
git push origin v1.1.0 && git push origin v1 --forceCodeQL analysis in the security scan only runs on forked actions that contain JavaScript or TypeScript code. Actions written in other languages (Go, Bash, Docker) still receive dependency review, composite action analysis, and diff summary checks, but no static analysis. CodeQL results are informational only and excluded from the aggregate security-scan status.
GitHub Actions rejects workflows when both the caller and the reusable workflow define concurrency blocks. All concurrency configuration belongs in the caller workflows, not in the reusable templates.
When called from a caller, job names are prefixed with the caller's job name. For example, if the caller job is named scan and the reusable workflow has a job dependency-review, the check name becomes scan / dependency-review. Branch protection rules must use the prefixed names.