Shared GitHub Actions and reusable workflows, used across SolidWorx projects and the open source repositories they depend on.
Everything here is referenced at @main. There are no release tags: a change
lands on main and every repository picks it up on its next run. That trades
the admin of tagging for the risk that a breaking change reaches every consumer
at once, so treat main as something that always has to work — the test suite
below is what makes that reasonable.
The repository has to stay public. GitHub only allows a private repository's actions and workflows to be reused inside the same organisation, and these are consumed from other organisations as well.
| Action | Purpose |
|---|---|
pr-milestone |
Put a pull request on the open milestone of the branch it targets |
Each one ships as both a composite action, at SolidWorx/actions/<name>@main,
and a reusable workflow, at SolidWorx/actions/.github/workflows/<name>.yml@main.
Use the reusable workflow unless you need to compose the step into a job you
already have — it carries the trigger conditions and concurrency settings that
would otherwise be copied into every repository.
Reusable workflows have to sit directly in .github/workflows/; GitHub does not
support subdirectories for them. Composite actions have no such restriction,
which is why the two live in different places.
Assigns the milestone a pull request belongs to, based on the branch it targets, so that a milestone always lists everything that will go into that release.
The milestone is looked up, not calculated. Every open milestone is mapped back to the branch it would be cut from, and the one that maps to the pull request's base branch wins. Nothing here invents a version number.
Check all three before adding this to a repository. The action is not useful without them and will fail loudly rather than guess.
- Release branches are named
X.xorX.Y.x—2.x,1.7.x,1.6.x. Pull requests targeting anything else are left alone. - Milestone titles are bare versions —
2.0.0,1.7.8. Novprefix, no extra words. - Exactly one milestone is open per release line.
2.0.0and2.1.0must not be open at the same time; once2.0.0closes, the next one is2.1.0for2.xor2.0.1for a new2.0.xbranch.
A milestone maps to a branch by trying the more specific branch first and falling back to the major one — the same rule a release script applies in reverse when it works out which branch to cut a closed milestone from:
| Milestone | Tries | Resolves to |
|---|---|---|
1.7.8 |
1.7.x, then 1.x |
1.7.x when that branch exists |
2.0.0 |
2.0.x, then 2.x |
2.x when there is no 2.0.x branch |
1.8.0 |
1.8.x, then 1.x |
1.x when there is no 1.8.x branch |
Create .github/workflows/milestone.yml:
name: Milestone
on:
pull_request_target:
types: [opened, reopened, edited]
jobs:
milestone:
uses: SolidWorx/actions/.github/workflows/pr-milestone.yml@main
permissions:
contents: read
issues: write
pull-requests: writeThat is the whole integration. Three things about it are deliberate:
pull_request_target, notpull_request. A pull request opened from a fork gets a read-only token underpull_requestand could never be assigned a milestone. This is safe here because nothing from the pull request is checked out or executed — the token is only used for the API call.- The permissions are granted by the caller. A reusable workflow can only narrow what it was given, so the block above is required; without it the assignment fails with a 403.
- No
actions/checkout. The runner fetches the action's own files, so the consuming repository is never cloned.
To assign milestones on demand as well — useful for catching up open pull requests after opening a new milestone — add a dispatch trigger:
on:
pull_request_target:
types: [opened, reopened, edited]
workflow_dispatch:
inputs:
pr:
description: "Pull request to put on its branch's milestone."
required: true
type: string
jobs:
milestone:
uses: SolidWorx/actions/.github/workflows/pr-milestone.yml@main
with:
pr: ${{ inputs.pr }}
permissions:
contents: read
issues: write
pull-requests: writeWhen the step has to go into a job you already have:
- name: Assign milestone
uses: SolidWorx/actions/pr-milestone@main
with:
pr: ${{ github.event.pull_request.number }}| Input | Default | Description |
|---|---|---|
pr |
required | Number of the pull request to put on a milestone |
repository |
github.repository |
Repository the pull request belongs to, as owner/name |
token |
github.token |
Token able to read milestones and write pull requests |
dry-run |
false |
Resolve the milestone and report it without assigning it |
Going this route means supplying the trigger conditions yourself. In particular,
an edited event fires for title and body changes too, so gate the job on the
base branch actually having changed or a milestone set by hand gets overwritten
on every description tweak:
if: github.event.action != 'edited' || github.event.changes.base.ref.from != null| Situation | Result |
|---|---|
| Base branch has one open milestone | Assigned, replacing whatever was there |
| Pull request is already on that milestone | Nothing, no API call |
| Base branch is not a release branch | Nothing, logged as a notice |
| Release branch has no open milestone | Fails the check |
| Release branch has more than one open milestone | Fails the check |
The last two are failures rather than silent skips because both mean the repository's milestones need attention: the first release cannot be cut, and the second makes the branch ambiguous.
The script underneath is standalone and takes a --dry-run, which is the
quickest way to confirm the conventions hold in a repository before wiring the
workflow up:
$ gh auth login
$ pr-milestone/pr-milestone --repo Payum/Payum --branch 2.x
2.x -> 2.0.0 (#2)
$ pr-milestone/pr-milestone --repo Payum/Payum 1053 --dry-run
Would put pull request #1053 on 2.0.0 (#2)--branch resolves and reports without touching the pull request, so it works
as a check of the mapping on its own. --repo defaults to GITHUB_REPOSITORY
under Actions and to origin otherwise, which is why it is needed when running
from a clone of this repository.
<name>/
├── action.yml composite action
├── <name> the implementation
└── test executable, exits non-zero on failure
.github/workflows/
└── <name>.yml reusable workflow wrapping the action
- The composite action reaches its own files through
$GITHUB_ACTION_PATH, so consumers never needactions/checkout. - A reusable workflow referencing an action from this repository must use the
full
SolidWorx/actions/<name>@mainpath. A relative./<name>resolves against the calling repository, not this one. - Do not declare
permissionsin a reusable workflow. It can only narrow what the caller granted, and fails outright against a caller that granted less. Document what the caller has to grant instead. testis picked up automatically by CI — no workflow changes needed. Stub the network at the CLI boundary, the waypr-milestone/testputs a fakeghonPATHand has it record calls instead of making them.
$ ./pr-milestone/test
ok - a minor branch resolves to its open milestone
...
13 passed, 0 failedCI runs actionlint, shellcheck over every executable bash script, and each
action's test. Since consumers track main, a red build means every repository
using these workflows is one merge away from breaking.