Skip to content

Release RC PR Workflow#

.github/workflows/release-rc-pr.yml — opens a PR to the default branch whenever a release candidate branch is created. The workflow serves two roles:

Role Trigger
ci-helpers-internal on: create — auto-fires when a release/* branch is pushed to ci-helpers itself
Caller reusable workflow_call — any repo can call it to get the same auto-PR behaviour

Required setup#

The workflow needs permission to create pull requests. GitHub provides two paths:

Settings → Actions → General → Workflow permissions → tick "Allow GitHub Actions to create and approve pull requests".

This lets the built-in GITHUB_TOKEN create PRs. No secrets required.

Option B — Add a GH_PAT secret#

If the repo setting above is disabled (e.g. org policy blocks it), add a repository secret named GH_PAT containing either:

  • a classic PAT with repo scope, or
  • a fine-grained PAT with Pull requests: Read and write access.

The workflow uses ${{ secrets.GH_PAT || github.token }} — it prefers the PAT when present and falls back to github.token otherwise.

Security note: store GH_PAT in the caller repo only, scoped to that repo. ci-helpers never stores a cross-repo token. The secret flows through GitHub's encrypted workflow_call secrets mechanism and is only visible inside the job that needs it.


Using this as a reusable workflow (caller repos)#

Add a small wrapper in your repo. The caller's on: create event provides github.ref_name which the called workflow reads directly — no extra inputs needed in the common case.

# .github/workflows/release-pr.yml  (in your repo)
name: Open release PR

on:
  create:

jobs:
  open-pr:
    if: ${{ github.event.ref_type == 'branch' && startsWith(github.event.ref, 'release/') }}
    uses: nikolareljin/ci-helpers/.github/workflows/release-rc-pr.yml@production
    secrets: inherit          # passes GH_PAT if you set it; falls back to github.token

The PR targets the repository's default branch. To target a different branch, pass base_branch (any value other than main is used as given):

    uses: nikolareljin/ci-helpers/.github/workflows/release-rc-pr.yml@production
    with:
      base_branch: master
    secrets: inherit

Why secrets: inherit is safe here#

  • The GITHUB_TOKEN the called workflow uses is scoped to your repo only. It cannot access ci-helpers or any other repo.
  • GH_PAT, if present, is a fine-grained PAT you stored in your own repo, scoped to your repo. Neither token gives ci-helpers any cross-repo access.
  • This is the same model all other ci-helpers workflow_call workflows use.

Inputs (workflow_call)#

Input Type Default Description
base_branch string main Branch the PR targets. Any value other than main is used as given. main -- also the default, so it cannot be told apart from passing nothing -- uses the repository's default branch from the event payload.
soft_fail boolean false When true, a known permission 403 (Actions may not open PRs, the token lacks pull-requests: write, PRs disabled) warns and the job passes. By default it fails the job. ci-helpers' own run on create (no inputs) warns, since its release script opens the PR.

Secrets (workflow_call)#

Secret Required Description
GH_PAT no PAT with pull-request write access (classic: repo scope; fine-grained: Pull requests Read+write); falls back to github.token

Behaviour#

  • Trigger (internal): on: create event — fires once when a branch is first pushed, not on subsequent commits to that branch.
  • Trigger (callers): workflow_call from a caller's wrapper -- on: create (filtered to release/* branches) or any other event, e.g. on: push to release/*.
  • Filter: only acts on branches matching release/[v]X.Y.Z, release/[v]X.Y.Z-rcN, or release/[v]X.Y.Z-rc.N.
  • Idempotent: if a PR already exists for that head/base pair, it does nothing.
  • Default branch detection:
  • workflow_call with a base_branch other than main: that branch.
  • Otherwise (base_branch is main, or ci-helpers' own on: create): context.payload.repository.default_branch from the event payload (no API call needed), then main.

Troubleshooting#

Two failures stop this workflow from opening a PR:

  1. 401 reading the repository. The workflow reads default_branch from the event payload and makes no repos.get() call, because actions/github-script under FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true gets 401 for that call. A copy that calls repos.get() fails this way.
  2. 403 GitHub Actions is not permitted to create or approve pull requests. The repository setting that lets Actions open PRs is off. Enable Option A above, or provide GH_PAT. The job fails with this message and the remediations when called; soft_fail: true turns it into a warning.

Example branches#

Branch Triggers?
release/1.2.3 yes
release/v1.2.3 yes
release/1.2.3-rc1 yes
release/1.2.3-rc.1 yes
release/1.2.3-hotfix no — non-numeric suffix
feature/1.2.3 no — wrong prefix

Before every full release: refresh the action pins#

A full release (X.Y.Z, not an -rcN) must update the third-party action SHAs first. Run it on the release branch, before cutting:

bash scripts/update_pinned_actions.sh          # rewrite stale pins
bash scripts/update_pinned_actions.sh --check  # must report 0 stale, 0 warnings

Why this is a release step and not left to the bots: Dependabot opens one pull request per action and moves the SHA without touching the version comment. Merging six of those leaves six pins reading # v1.321.0 @ 2026-09-10 while pointing at v1.324.0. scripts/update_pinned_actions.sh and the security audit both read those comments, so the repository ends up reporting a version it does not ship. The plain run above reports each such pin as NEWER and moves its comment to the tag the SHA is, with today's date; the SHA stays.

A SHA verified as a newer release tag of the same action passes ordinary work: workflow-yaml-check.yml on push and on other pull requests, security-weekly.yml, and auto-tag-release-push.yml on the merge of a pull request from any other branch all run --check --allow-newer-tag. A plain --check runs on a pull request whose head ref starts with release/, on a release merge or any push auto-tag-release-push.yml cannot place, and in production-branch.yml. A comment left behind fails those until the plain run above has corrected it.

The plain run warns and leaves the line alone when it cannot tell, or when the SHA is a newer release of another shape (# v1 at the commit of v2.0.0, with no v2 tag). Correct that comment by hand.

Without this step, a consumer pinned to @production gets an action version nobody recorded.

Two things to check by hand when a Dependabot pin looks wrong:

  • git/ref/tags/<tag> returns the tag object for an annotated tag, not the commit. Dereference it before comparing SHAs, or github/codeql-action will look like a mismatch when it is correct.
  • The comment format is # vX.Y.Z @ YYYY-MM-DD, with the date of the bump, not the date of the upstream release.

A release merge fails the pin gate#

A Dependabot bump can land on main after the release pull request went green. The merge then fails the plain --check in auto-tag-release-push.yml, so no tag is cut. A re-run checks the same commit and fails the same way, unless the failure was only a lookup warning, which a re-run can clear.

Recover with a new pull request from the same head ref, while tag X.Y.Z does not exist. The release ruleset blocks deleting the branch, so reuse it:

git fetch origin
git switch release/X.Y.Z
git pull --ff-only
git merge origin/main
bash scripts/update_pinned_actions.sh          # moves the comments
bash scripts/update_pinned_actions.sh --check  # must report 0 stale, 0 warnings
git commit -am "Refresh action pin comments for X.Y.Z"
git push
gh pr create --base main --head release/X.Y.Z --fill

auto-tag.yml keys on the head ref of the pull request merged as the pushed commit and refuses only a tag that exists, so this merge cuts the tag, and for a final X.Y.Z moves production. A different branch name cuts nothing.