Skip to content

Commit 2440cd0

Browse files
silehtclaude
andauthored
feat: bootstrap setup-cli composite action (#1)
Install the Mergify CLI (mergify-cli) with version pinning and Renovate autoupdate, extracted from gha-mergify-ci's install steps. - action.yml: setup-python -> setup-uv -> uv tool install mergify-cli (pinned default, `latest`/empty supported), exposes the resolved installed version as the `mergify_cli_version` output (read from `uv tool list`, since `mergify --version` may print a placeholder). - renovate.json: config:best-practices + chore semantic commits, pypi minimumReleaseAge for mergify-cli, custom managers that bump the `# renovate:` default in action.yml and keep the README inputs table and the self-referenced `@vN` usage example in sync. - generate-doc.py/.sh: auto-generate the README inputs table from action.yml; CI fails if it drifts. - ci.yaml: actionlint + yamllint + semgrep, autodoc sync check, and a dogfood job that runs `./` (pinned and `latest`) and asserts the output + `mergify --version`. - release.yml, .yamllint.yml mirrored from gha-mergify-ci. Fixes MRGFY-7628 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dff542a commit 2440cd0

8 files changed

Lines changed: 379 additions & 1 deletion

File tree

.github/release.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
changelog:
2+
exclude:
3+
authors:
4+
- dependabot[bot]
5+
- mergify[bot]

.github/workflows/ci.yaml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Continuous Integration
2+
permissions: read-all
3+
4+
on:
5+
pull_request:
6+
branches:
7+
- main
8+
- devs/**
9+
10+
concurrency:
11+
# yamllint disable-line rule:line-length
12+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
linters:
17+
timeout-minutes: 5
18+
runs-on: ubuntu-24.04
19+
steps:
20+
- name: Checkout 🛎️
21+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
22+
23+
- name: Setup Python 🔧
24+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
25+
with:
26+
python-version: 3.14.5
27+
28+
- name: Check workflow files
29+
uses: docker://rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667
30+
with:
31+
args: -color
32+
33+
- name: Test 🔍
34+
run: |
35+
# nosemgrep: generic.ci.security.use-frozen-lockfile.use-frozen-lockfile-pip
36+
pip install semgrep yamllint
37+
semgrep --config=auto --error
38+
yamllint .
39+
40+
autodoc:
41+
timeout-minutes: 5
42+
runs-on: ubuntu-24.04
43+
steps:
44+
- name: Checkout
45+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
46+
47+
- name: Install uv
48+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
49+
with:
50+
enable-cache: false
51+
52+
- name: Regenerate documentation
53+
run: ./generate-doc.sh
54+
55+
- name: Verify Changed files
56+
run: |
57+
if ! git diff --exit-code -- README.md; then
58+
echo "::error::Action documentation is out of date. Run \`./generate-doc.sh\`"
59+
exit 1
60+
fi
61+
62+
test-install:
63+
timeout-minutes: 5
64+
runs-on: ubuntu-24.04
65+
steps:
66+
- name: Checkout 🛎️
67+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
68+
69+
- name: Install pinned mergify-cli
70+
id: pinned
71+
uses: ./
72+
73+
- name: Assert pinned install
74+
env:
75+
VERSION: ${{ steps.pinned.outputs.mergify_cli_version }}
76+
run: |
77+
test -n "$VERSION"
78+
mergify --version
79+
80+
- name: Install latest mergify-cli
81+
id: latest
82+
uses: ./
83+
with:
84+
mergify_cli_version: latest
85+
86+
- name: Assert latest install
87+
env:
88+
VERSION: ${{ steps.latest.outputs.mergify_cli_version }}
89+
run: |
90+
test -n "$VERSION"
91+
mergify --version
92+
93+
all-greens:
94+
if: ${{ !cancelled() }}
95+
needs:
96+
- linters
97+
- autodoc
98+
- test-install
99+
runs-on: ubuntu-latest
100+
steps:
101+
- name: Verify all jobs succeeded
102+
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
103+
with:
104+
jobs: ${{ toJSON(needs) }}

.yamllint.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
extends: default
2+
ignore: |
3+
.git
4+
docs/node_modules
5+
zfixtures
6+
.venv
7+
rules:
8+
document-start: disable
9+
truthy: disable
10+
comments:
11+
level: error
12+
# Renovate pins digests as "@<sha> # v1.2.3" with a single space before the
13+
# comment; allow it instead of yamllint's default of 2.
14+
min-spaces-from-content: 1
15+
# Buggy checks:
16+
# https://github.com/adrienverge/yamllint/issues/375
17+
# https://github.com/adrienverge/yamllint/issues/141
18+
# https://github.com/adrienverge/yamllint/issues/384
19+
comments-indentation: disable
20+
# Renovate pins actions/images to digests, producing lines that exceed any
21+
# sensible width (e.g. docker://...@sha256:<64 hex>). Disable the check so
22+
# renovate can pin freely without per-line opt-outs.
23+
line-length: disable
24+
quoted-strings:
25+
quote-type: double
26+
required: only-when-needed
27+
allow-quoted-quotes: true

README.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,47 @@
11
# setup-cli
2-
GitHub Action to install the Mergify CLI (mergify-cli) with version pinning and Renovate autoupdate
2+
3+
GitHub Action to install the [Mergify CLI](https://pypi.org/project/mergify-cli/)
4+
(`mergify-cli`) with version pinning and Renovate autoupdate.
5+
6+
It sets up Python, installs `uv`, then installs `mergify-cli` (pinned by default,
7+
`latest` supported) and exposes the resolved version as an output.
8+
9+
More information on https://mergify.com
10+
11+
## Usage
12+
13+
Pin the action to a released major (see the [releases](https://github.com/Mergifyio/setup-cli/releases)):
14+
15+
```yaml
16+
- uses: Mergifyio/setup-cli@v1
17+
18+
- run: mergify --version
19+
```
20+
21+
Pin a specific `mergify-cli` version, or install the latest one:
22+
23+
```yaml
24+
- uses: Mergifyio/setup-cli@v1
25+
id: setup-cli
26+
with:
27+
mergify_cli_version: latest
28+
29+
- run: echo "Installed mergify-cli ${{ steps.setup-cli.outputs.mergify_cli_version }}"
30+
```
31+
32+
## Inputs
33+
34+
<!-- AUTO-DOC-INPUT:START - Do not remove or modify this section -->
35+
36+
| Input | Type | Required | Default | Description |
37+
| --- | --- | --- | --- | --- |
38+
| `mergify_cli_version` | string | false | `2026.6.8.1` | Version of mergify-cli to install. Use `latest` to install the latest released version without pinning. |
39+
| `python_version` | string | false | `3.14` | Python version to set up for the install (passed to actions/setup-python). |
40+
41+
<!-- AUTO-DOC-INPUT:END -->
42+
43+
## Outputs
44+
45+
| Output | Description |
46+
| --- | --- |
47+
| `mergify_cli_version` | The `mergify-cli` version that was installed. Resolved from the installed package metadata, so it reflects the real version even when `latest` or an empty input was requested. |

action.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Setup Mergify CLI
2+
description: Install the Mergify CLI (mergify-cli) with version pinning.
3+
author: Mergify
4+
branding:
5+
icon: terminal
6+
color: blue
7+
inputs:
8+
mergify_cli_version:
9+
description: |
10+
Version of mergify-cli to install. Use `latest` to install the latest
11+
released version without pinning.
12+
# renovate: datasource=pypi depName=mergify-cli
13+
default: 2026.6.8.1
14+
python_version:
15+
description: Python version to set up for the install (passed to actions/setup-python).
16+
default: "3.14"
17+
outputs:
18+
mergify_cli_version:
19+
description: |
20+
The mergify-cli version that was installed. Resolved from the installed
21+
package metadata, so it reflects the real version even when `latest` or an
22+
empty input was requested.
23+
value: ${{ steps.install.outputs.mergify_cli_version }}
24+
runs:
25+
using: composite
26+
steps:
27+
- name: Setup Python 🔧
28+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
29+
with:
30+
python-version: ${{ inputs.python_version }}
31+
32+
- name: Install uv
33+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
34+
with:
35+
# mergify is too small to benefit + there is no version lock (so no cache key either)
36+
enable-cache: false
37+
38+
- name: Install mergify-cli
39+
id: install
40+
shell: bash
41+
env:
42+
MERGIFY_CLI_VERSION: ${{ inputs.mergify_cli_version }}
43+
run: |
44+
if [ -z "$MERGIFY_CLI_VERSION" ] || [ "$MERGIFY_CLI_VERSION" = "latest" ]; then
45+
# --upgrade implies --refresh, so uv re-resolves against PyPI and
46+
# installs the newest release even on persistent self-hosted runners
47+
# where an older mergify-cli is already cached.
48+
uv tool install --upgrade mergify-cli
49+
else
50+
uv tool install "mergify-cli==$MERGIFY_CLI_VERSION"
51+
fi
52+
mergify --version
53+
# `mergify --version` may print a placeholder while versioning becomes
54+
# Rust-native, so read the resolved version from the package metadata.
55+
installed=$(uv tool list | awk '/^mergify-cli /{print $2}' | sed 's/^v//')
56+
echo "mergify_cli_version=$installed" >> "$GITHUB_OUTPUT"

generate-doc.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# /// script
2+
# requires-python = ">=3.11"
3+
# dependencies = ["pyyaml"]
4+
# ///
5+
"""Generate the README Inputs table from action.yml.
6+
7+
Replaces tj-actions/auto-doc: parses the action's inputs and rewrites the
8+
GitHub-flavoured Markdown table between the AUTO-DOC-INPUT markers in README.md.
9+
"""
10+
11+
import pathlib
12+
import re
13+
14+
import yaml
15+
16+
ROOT = pathlib.Path(__file__).parent
17+
ACTION = ROOT / "action.yml"
18+
README = ROOT / "README.md"
19+
START = "<!-- AUTO-DOC-INPUT:START - Do not remove or modify this section -->"
20+
END = "<!-- AUTO-DOC-INPUT:END -->"
21+
22+
23+
def render_description(text: str) -> str:
24+
"""Render an action.yml description as a single Markdown table cell.
25+
26+
Wrapped prose lines are joined with spaces; `*`-prefixed lines (e.g. the
27+
list of actions) become `<br>`-separated bullets so they render as a list
28+
inside the cell rather than a run of literal asterisks.
29+
"""
30+
parts: list[str] = []
31+
for raw in text.strip().splitlines():
32+
line = raw.strip()
33+
if not line:
34+
continue
35+
if line.startswith("* "):
36+
parts.append("<br>• " + line[2:].strip())
37+
elif parts:
38+
parts[-1] += " " + line
39+
else:
40+
parts.append(line)
41+
return "".join(parts)
42+
43+
44+
def render_table(inputs: dict) -> str:
45+
rows = [
46+
"| Input | Type | Required | Default | Description |",
47+
"| --- | --- | --- | --- | --- |",
48+
]
49+
for name in sorted(inputs):
50+
spec = inputs[name] or {}
51+
required = "true" if spec.get("required") else "false"
52+
default = spec.get("default")
53+
default_cell = f"`{default}`" if default not in (None, "") else ""
54+
description = render_description(str(spec.get("description", "")))
55+
rows.append(f"| `{name}` | string | {required} | {default_cell} | {description} |")
56+
return "\n".join(rows)
57+
58+
59+
def main() -> None:
60+
action = yaml.safe_load(ACTION.read_text(encoding="utf-8"))
61+
table = render_table(action.get("inputs") or {})
62+
block = f"{START}\n\n{table}\n\n{END}"
63+
64+
readme = README.read_text(encoding="utf-8")
65+
pattern = re.escape(START) + r".*?" + re.escape(END)
66+
if not re.search(pattern, readme, flags=re.DOTALL):
67+
raise SystemExit("AUTO-DOC-INPUT markers not found in README.md")
68+
69+
new = re.sub(pattern, lambda _: block, readme, flags=re.DOTALL)
70+
README.write_text(new, encoding="utf-8", newline="\n")
71+
72+
73+
if __name__ == "__main__":
74+
main()

generate-doc.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
command -v uv >/dev/null 2>&1 || { echo "uv is not installed: https://docs.astral.sh/uv/" >&2; exit 1; }
6+
7+
exec uv run "$(dirname "$0")/generate-doc.py"

renovate.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
3+
"extends": [
4+
"config:best-practices",
5+
":semanticCommitTypeAll(chore)"
6+
],
7+
"prHourlyLimit": 10,
8+
"rebaseWhen": "conflicted",
9+
"minimumReleaseAge": "7 days",
10+
"osvVulnerabilityAlerts": true,
11+
"lockFileMaintenance": { "enabled": true },
12+
"packageRules": [
13+
{
14+
"matchDatasources": ["pypi"],
15+
"matchPackageNames": ["mergify-cli"],
16+
"minimumReleaseAge": "2 days"
17+
},
18+
{
19+
"description": "Bump the self-referenced action version immediately: it is our own freshly-released major (README usage example + ci.yaml dogfooding), so the third-party stability wait is pointless.",
20+
"matchDepNames": ["Mergifyio/setup-cli"],
21+
"minimumReleaseAge": "0"
22+
}
23+
],
24+
"customManagers": [
25+
{
26+
"customType": "regex",
27+
"managerFilePatterns": [
28+
"/^action\\.yml$/"
29+
],
30+
"matchStrings": [
31+
"# renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>[^\\s]+)(?: versioning=(?<versioning>[a-z0-9-]+))?\\s+default:\\s*(?<currentValue>[^\\s]+)"
32+
]
33+
},
34+
{
35+
"description": "Keep the auto-generated README inputs table in sync with the mergify_cli_version default in action.yml, so the bump PR lands both files in one go and the autodoc check passes.",
36+
"customType": "regex",
37+
"managerFilePatterns": [
38+
"/^README\\.md$/"
39+
],
40+
"matchStrings": [
41+
"\\| `mergify_cli_version` \\| string \\| \\w+ \\| `(?<currentValue>[^`]+)` \\|"
42+
],
43+
"datasourceTemplate": "pypi",
44+
"depNameTemplate": "mergify-cli"
45+
},
46+
{
47+
"description": "Bump the self-referenced action version pinned in the README usage example on each new setup-cli release.",
48+
"customType": "regex",
49+
"managerFilePatterns": [
50+
"/^README\\.md$/"
51+
],
52+
"matchStrings": [
53+
"uses: Mergifyio/setup-cli@(?<currentValue>v\\d+)"
54+
],
55+
"datasourceTemplate": "github-tags",
56+
"depNameTemplate": "Mergifyio/setup-cli",
57+
"versioningTemplate": "github-actions"
58+
}
59+
]
60+
}

0 commit comments

Comments
 (0)