GitHub Actions Automation skill for Codex
Repo housekeeping bots with least privilege. Codex writes housekeeping workflows for labels, stale issues, greetings, triage, Dependabot auto-merge and release notes, each with explicit per-job permissions and a manual dry-run trigger. It keeps pull_request_target away from fork code, routes untrusted titles through env or github-script instead of shell, and explains why GITHUB_TOKEN events never start a second workflow.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- github-actions-automation Installs to ~/.agents/skills/github-actions-automation/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install github-actions-automation The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project github-actions-automation Installs to .agents/skills/github-actions-automation/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/github-actions-automation.zipunzip -o github-actions-automation.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) github-actions-automation PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $github-actions-automation, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Automates GitHub repository chores with Actions - PR labeling by path, stale issue and PR cleanup, first-time contributor greetings, issue triage with actions/github-script, Dependabot auto-merge, release drafting with release-drafter or release-please - using least-privilege permissions and safe pull_request_target patterns. Use when the user wants to auto-label PRs, close stale issues, auto-merge Dependabot patches, draft release notes, triage new issues, or a bot workflow fails with 403 or never triggers.
Use it when
- Maintainers spend time labeling, triaging or closing stale items by hand
- Dependabot PRs pile up and safe updates should merge themselves
- Release notes should be drafted from merged PRs
- A bot workflow fails with 403 or never triggers
Use something else when
- You are building CI test and build pipelines: use github-actions-workflows
- You need to audit workflows for injection and token risks: use github-actions-security
- The bot needs its own identity and webhooks across many repos: use github-apps
How to check the result
actionlint passes, a workflow_dispatch dry run lists a plausible set of issues or PRs, and a test PR from a fork gets labeled and greeted under pull_request_target.
The mistake it prevents
Checking out and running fork code inside a pull_request_target job, which hands the fork a write token and repository secrets.
Done well means: each chore runs with only the permissions it needs, cannot be hijacked
by a fork’s code, does not spam contributors, and a maintainer can preview its effect
before it touches hundreds of issues. This skill is about housekeeping bots; build and
test pipelines belong to $github-actions-workflows.
Inspect first
ls .github/workflows .github/*.yml 2>/dev/null
gh api repos/{owner}/{repo} --jq '{default_branch, allow_auto_merge, delete_branch_on_merge}'
gh label list --limit 200 # labels the automation will use
gh api repos/{owner}/{repo}/actions/permissions/workflow # default GITHUB_TOKEN scope
Read AGENTS.md and CONTRIBUTING.md for label names and release conventions. Reuse
existing labels; creating a parallel taxonomy confuses triage. gh calls need network
and auth; in an offline sandbox, write the files and list the commands for the user.
Pick the chore’s tool
| Chore | Default | Trigger |
|---|---|---|
| Label PRs by changed paths | actions/labeler@v5 + .github/labeler.yml | pull_request_target |
| Label by PR size, title or body | actions/github-script@v7 | pull_request_target |
| Mark and close stale issues/PRs | actions/stale@v9 | schedule + workflow_dispatch |
| Lock old closed threads | dessant/lock-threads@v5 | schedule |
| Greet first-time contributors | actions/github-script@v7 | pull_request_target, issues |
| Auto-merge Dependabot patch/minor | dependabot/fetch-metadata@v2 + gh pr merge --auto | pull_request |
| Draft release notes from merged PRs | release-drafter/release-drafter@v6 | push to default branch |
| Versioned releases from Conventional Commits | googleapis/release-please-action@v4 | push to default branch |
| Anything custom with the REST API | actions/github-script@v7 | as needed |
Full, copyable workflow files for each row are in references/recipes.md. Read it when writing one of these workflows; adapt labels and thresholds to the repo.
Hard rules
- Explicit permissions on every workflow. Start from
permissions: {}at the top and grant per job:issues: write,pull-requests: write,contents: writeonly for release jobs. pull_request_targetnever checks out or runs the PR’s code. It runs with a write token and secrets in the base repo context. Labeler and greeting jobs only read metadata through the API, which is safe. If a job must build PR code, usepull_request(read-only token for forks).- Do not interpolate untrusted text into
run:.${{ github.event.issue.title }}inside a shell script is command injection. Pass it throughenv:and quote it, or read it insidegithub-scriptfromcontext.payload. - Pin actions. Use a major tag from a trusted publisher at minimum; pin third-party actions to a full commit SHA with a comment naming the version.
- Dry-run bulk changes.
actions/stalehasdebug-only: true; custom scripts get adry_runinput onworkflow_dispatch. Run once in dry-run and read the log before the first real run touches existing issues.
Events and tokens: why a bot “never triggers”
- Events created with the default
GITHUB_TOKEN(a label added, a PR opened, a release published) do not start other workflows, exceptworkflow_dispatchandrepository_dispatch. To chain workflows, use a GitHub App token (actions/create-github-app-token) or trigger the second workflow explicitly. scheduleruns only on the default branch, in UTC, may start late, and is disabled after 60 days without repository activity in public repos. Always addworkflow_dispatch.- Dependabot-triggered
pull_requestruns get a read-only token and only Dependabot secrets; grant write permissions explicitly in the workflow. 403 Resource not accessible by integrationmeans the job lacks a permission scope or the org restricts the default token. Add the scope; do not swap in a personal token.
github-script essentials
- uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const changed = pr.additions + pr.deletions;
const size = changed < 50 ? 'size/S' : changed < 400 ? 'size/M' : 'size/L';
const stale = pr.labels.map(l => l.name).filter(n => n.startsWith('size/') && n !== size);
for (const name of stale) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name });
}
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [size] });
githubis an authenticated Octokit;contextholds the event. PRs are issues for labels and comments (issues.*endpoints with the PR number).- Paginate with
github.paginate(github.rest.issues.listForRepo, {...})when scanning. - Add labels only if they exist, or create them first;
addLabelssilently creates labels with default grey colour.
Workflow for adding a chore
- [ ] 1. Confirm labels, thresholds and messages with the maintainer
- [ ] 2. Write the workflow with explicit permissions and workflow_dispatch
- [ ] 3. Validate syntax: actionlint (if installed) and yamllint
- [ ] 4. Push to a branch; run in dry-run via workflow_dispatch where supported
- [ ] 5. Read the run log; check the planned changes match intent
- [ ] 6. Merge; watch the first scheduled or event-triggered run
Workflows that comment on, close, lock or merge items act publicly on the user’s repository. Show the user the messages and thresholds and get a yes before enabling them on the default branch.
Gotchas
actions/staleprocesses a limited number of API operations per run (operations-per-run, default 30); a large backlog takes several runs. Raise it gradually and watch rate limits.- Stale bots on a project with slow maintainers alienate contributors. Exempt labels like
confirmed,securityandpinned, and set a longdays-before-stale(60 to 90). gh pr merge --autorequires “Allow auto-merge” in repo settings and branch protection with required checks; otherwise it merges immediately or fails.- Release jobs need
contents: write; release-please also needspull-requests: write. Org settings may forbid Actions from creating PRs (“Allow GitHub Actions to create and approve pull requests”). - Labeler v5 changed the config format (
changed-fileswithany-glob-to-any-file); v4-style configs fail with a parse error.
Verification
actionlintpasses (it also catches untrusted-input injection inrun:).- Dry-run log lists the issues or PRs it would touch, and the count is plausible.
- Open a test PR from a fork or a second account to confirm labeler and greeting work
under
pull_request_target. - For release automation, confirm the draft or release PR content on a branch merge before the first tag is published.
Summary for the maintainer
**Added:** .github/workflows/stale.yml, .github/workflows/labeler.yml, .github/labeler.yml
**Permissions:** stale: issues/pull-requests write; labeler: pull-requests write, contents read
**Behaviour:** issues stale after 90 days, closed 14 days later; exempt: confirmed, security
**Dry run:** stale debug-only run on 2026-10-28 would mark 41 issues, close 0
**To disable:** delete the workflow file or `gh workflow disable stale.yml` What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "GitHub Actions Automation"
short_description: "Repo housekeeping bots with least privilege"
default_prompt: "$github-actions-automation Auto-label PRs by folder and close issues with no activity for 90 days" references/recipes.md
# Repo chore recipes
Copy, then adjust labels, thresholds and messages to the repository. Every recipe has
explicit permissions and a manual trigger where it makes sense.
## Path labels (labeler v5)
`.github/labeler.yml`:
```yaml
area/docs:
- changed-files:
- any-glob-to-any-file: ["docs/**", "**/*.md"]
area/api:
- changed-files:
- any-glob-to-any-file: ["src/api/**", "openapi/**"]
dependencies:
- changed-files:
- any-glob-to-any-file: ["package.json", "pnpm-lock.yaml", "requirements*.txt"]
```
`.github/workflows/labeler.yml`:
```yaml
name: Label PRs
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
label:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@v5
with:
sync-labels: true # remove path labels that no longer apply
```
No checkout step: labeler reads the changed-file list through the API.
## Stale issues and PRs
```yaml
name: Stale
on:
schedule:
- cron: "27 3 * * *"
workflow_dispatch:
inputs:
debug:
description: "Dry run (log only)"
type: boolean
default: true
permissions: {}
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
debug-only: ${{ github.event_name == 'workflow_dispatch' && inputs.debug }}
days-before-stale: 90
days-before-close: 14
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: confirmed,security,pinned
exempt-pr-labels: security,work-in-progress
exempt-draft-pr: true
stale-issue-message: >
This issue has had no activity for 90 days. It will close in 14 days
unless someone comments or a maintainer adds the `confirmed` label.
close-issue-message: Closed for inactivity. Comment if it is still relevant and we will reopen.
stale-pr-message: >
This PR has had no activity for 90 days. Push a commit or comment to keep it open.
operations-per-run: 60
```
## First-time contributor greeting
```yaml
name: Greet
on:
pull_request_target:
types: [opened]
permissions: {}
jobs:
greet:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const assoc = pr.author_association;
if (!['FIRST_TIME_CONTRIBUTOR', 'FIRST_TIMER'].includes(assoc)) return;
await github.rest.issues.createComment({
...context.repo,
issue_number: pr.number,
body: [
`Thanks for your first PR, @${pr.user.login}.`,
'A maintainer will review within a week. CONTRIBUTING.md lists the checks CI runs.',
].join('\n\n'),
});
```
## Issue triage: require a reproduction
```yaml
name: Triage new issues
on:
issues:
types: [opened]
permissions: {}
jobs:
triage:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const body = issue.body || '';
const labels = ['needs-triage'];
if (/^### Steps to reproduce\s*\n+\s*(_No response_)?\s*$/m.test(body) || !/Steps to reproduce/i.test(body)) {
labels.push('needs-repro');
}
await github.rest.issues.addLabels({ ...context.repo, issue_number: issue.number, labels });
```
The issue title and body are untrusted input: they are read inside JavaScript here, never
interpolated into a shell `run:` line.
## Dependabot auto-merge (patch and minor)
```yaml
name: Dependabot auto-merge
on: pull_request
permissions: {}
jobs:
automerge:
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- id: meta
uses: dependabot/fetch-metadata@v2
- if: steps.meta.outputs.update-type != 'version-update:semver-major'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
Requires "Allow auto-merge" and branch protection with required status checks, so the
merge waits for CI. Major versions stay manual.
## Release notes draft (release-drafter)
`.github/release-drafter.yml`:
```yaml
name-template: "v$RESOLVED_VERSION"
tag-template: "v$RESOLVED_VERSION"
categories:
- title: Features
labels: [feature, enhancement]
- title: Fixes
labels: [bug, fix]
- title: Maintenance
labels: [chore, dependencies]
version-resolver:
major: { labels: [breaking] }
minor: { labels: [feature, enhancement] }
default: patch
template: |
$CHANGES
```
```yaml
name: Release drafter
on:
push:
branches: [main]
workflow_dispatch:
permissions: {}
jobs:
draft:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
The draft is never published automatically; a maintainer edits and publishes it.
## Release PRs from Conventional Commits (release-please)
```yaml
name: release-please
on:
push:
branches: [main]
permissions: {}
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: node # or python, go, simple; or use a manifest config file
```
Merging the release PR creates the tag and GitHub release. A publish job (npm, PyPI) that
should run on that release must be triggered by the action's `release_created` output in
the same workflow, because a release created by `GITHUB_TOKEN` does not trigger other
workflows.
## Lock old closed threads
```yaml
name: Lock threads
on:
schedule:
- cron: "41 4 * * 1"
workflow_dispatch:
permissions: {}
jobs:
lock:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: 180
pr-inactive-days: 180
issue-comment: >
Locked because it has been closed for 180 days. Open a new issue and link this one.
```
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.