diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..72f7ec3 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,5 @@ +changelog: + exclude: + authors: + - dependabot[bot] + - mergify[bot] diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..e742121 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,104 @@ +name: Continuous Integration +permissions: read-all + +on: + pull_request: + branches: + - main + - devs/** + +concurrency: + # yamllint disable-line rule:line-length + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + linters: + timeout-minutes: 5 + runs-on: ubuntu-24.04 + steps: + - name: Checkout 🛎️ + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Setup Python 🔧 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: 3.14.5 + + - name: Check workflow files + uses: docker://rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 + with: + args: -color + + - name: Test 🔍 + run: | + # nosemgrep: generic.ci.security.use-frozen-lockfile.use-frozen-lockfile-pip + pip install semgrep yamllint + semgrep --config=auto --error + yamllint . + + autodoc: + timeout-minutes: 5 + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - name: Install uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + enable-cache: false + + - name: Regenerate documentation + run: ./generate-doc.sh + + - name: Verify Changed files + run: | + if ! git diff --exit-code -- README.md; then + echo "::error::Action documentation is out of date. Run \`./generate-doc.sh\`" + exit 1 + fi + + test-install: + timeout-minutes: 5 + runs-on: ubuntu-24.04 + steps: + - name: Checkout 🛎️ + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Install pinned mergify-cli + id: pinned + uses: ./ + + - name: Assert pinned install + env: + VERSION: ${{ steps.pinned.outputs.mergify_cli_version }} + run: | + test -n "$VERSION" + mergify --version + + - name: Install latest mergify-cli + id: latest + uses: ./ + with: + mergify_cli_version: latest + + - name: Assert latest install + env: + VERSION: ${{ steps.latest.outputs.mergify_cli_version }} + run: | + test -n "$VERSION" + mergify --version + + all-greens: + if: ${{ !cancelled() }} + needs: + - linters + - autodoc + - test-install + runs-on: ubuntu-latest + steps: + - name: Verify all jobs succeeded + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..7b7f1cf --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,27 @@ +extends: default +ignore: | + .git + docs/node_modules + zfixtures + .venv +rules: + document-start: disable + truthy: disable + comments: + level: error + # Renovate pins digests as "@ # v1.2.3" with a single space before the + # comment; allow it instead of yamllint's default of 2. + min-spaces-from-content: 1 + # Buggy checks: + # https://github.com/adrienverge/yamllint/issues/375 + # https://github.com/adrienverge/yamllint/issues/141 + # https://github.com/adrienverge/yamllint/issues/384 + comments-indentation: disable + # Renovate pins actions/images to digests, producing lines that exceed any + # sensible width (e.g. docker://...@sha256:<64 hex>). Disable the check so + # renovate can pin freely without per-line opt-outs. + line-length: disable + quoted-strings: + quote-type: double + required: only-when-needed + allow-quoted-quotes: true diff --git a/README.md b/README.md index 136e491..6976f80 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,47 @@ # setup-cli -GitHub Action to install the Mergify CLI (mergify-cli) with version pinning and Renovate autoupdate + +GitHub Action to install the [Mergify CLI](https://pypi.org/project/mergify-cli/) +(`mergify-cli`) with version pinning and Renovate autoupdate. + +It sets up Python, installs `uv`, then installs `mergify-cli` (pinned by default, +`latest` supported) and exposes the resolved version as an output. + +More information on https://mergify.com + +## Usage + +Pin the action to a released major (see the [releases](https://github.com/Mergifyio/setup-cli/releases)): + +```yaml +- uses: Mergifyio/setup-cli@v1 + +- run: mergify --version +``` + +Pin a specific `mergify-cli` version, or install the latest one: + +```yaml +- uses: Mergifyio/setup-cli@v1 + id: setup-cli + with: + mergify_cli_version: latest + +- run: echo "Installed mergify-cli ${{ steps.setup-cli.outputs.mergify_cli_version }}" +``` + +## Inputs + + + +| Input | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `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. | +| `python_version` | string | false | `3.14` | Python version to set up for the install (passed to actions/setup-python). | + + + +## Outputs + +| Output | Description | +| --- | --- | +| `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. | diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..5e14c59 --- /dev/null +++ b/action.yml @@ -0,0 +1,56 @@ +name: Setup Mergify CLI +description: Install the Mergify CLI (mergify-cli) with version pinning. +author: Mergify +branding: + icon: terminal + color: blue +inputs: + mergify_cli_version: + description: | + Version of mergify-cli to install. Use `latest` to install the latest + released version without pinning. + # renovate: datasource=pypi depName=mergify-cli + default: 2026.6.8.1 + python_version: + description: Python version to set up for the install (passed to actions/setup-python). + default: "3.14" +outputs: + mergify_cli_version: + description: | + 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. + value: ${{ steps.install.outputs.mergify_cli_version }} +runs: + using: composite + steps: + - name: Setup Python 🔧 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ inputs.python_version }} + + - name: Install uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + # mergify is too small to benefit + there is no version lock (so no cache key either) + enable-cache: false + + - name: Install mergify-cli + id: install + shell: bash + env: + MERGIFY_CLI_VERSION: ${{ inputs.mergify_cli_version }} + run: | + if [ -z "$MERGIFY_CLI_VERSION" ] || [ "$MERGIFY_CLI_VERSION" = "latest" ]; then + # --upgrade implies --refresh, so uv re-resolves against PyPI and + # installs the newest release even on persistent self-hosted runners + # where an older mergify-cli is already cached. + uv tool install --upgrade mergify-cli + else + uv tool install "mergify-cli==$MERGIFY_CLI_VERSION" + fi + mergify --version + # `mergify --version` may print a placeholder while versioning becomes + # Rust-native, so read the resolved version from the package metadata. + installed=$(uv tool list | awk '/^mergify-cli /{print $2}' | sed 's/^v//') + echo "mergify_cli_version=$installed" >> "$GITHUB_OUTPUT" diff --git a/generate-doc.py b/generate-doc.py new file mode 100644 index 0000000..2676568 --- /dev/null +++ b/generate-doc.py @@ -0,0 +1,74 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = ["pyyaml"] +# /// +"""Generate the README Inputs table from action.yml. + +Replaces tj-actions/auto-doc: parses the action's inputs and rewrites the +GitHub-flavoured Markdown table between the AUTO-DOC-INPUT markers in README.md. +""" + +import pathlib +import re + +import yaml + +ROOT = pathlib.Path(__file__).parent +ACTION = ROOT / "action.yml" +README = ROOT / "README.md" +START = "" +END = "" + + +def render_description(text: str) -> str: + """Render an action.yml description as a single Markdown table cell. + + Wrapped prose lines are joined with spaces; `*`-prefixed lines (e.g. the + list of actions) become `
`-separated bullets so they render as a list + inside the cell rather than a run of literal asterisks. + """ + parts: list[str] = [] + for raw in text.strip().splitlines(): + line = raw.strip() + if not line: + continue + if line.startswith("* "): + parts.append("
• " + line[2:].strip()) + elif parts: + parts[-1] += " " + line + else: + parts.append(line) + return "".join(parts) + + +def render_table(inputs: dict) -> str: + rows = [ + "| Input | Type | Required | Default | Description |", + "| --- | --- | --- | --- | --- |", + ] + for name in sorted(inputs): + spec = inputs[name] or {} + required = "true" if spec.get("required") else "false" + default = spec.get("default") + default_cell = f"`{default}`" if default not in (None, "") else "" + description = render_description(str(spec.get("description", ""))) + rows.append(f"| `{name}` | string | {required} | {default_cell} | {description} |") + return "\n".join(rows) + + +def main() -> None: + action = yaml.safe_load(ACTION.read_text(encoding="utf-8")) + table = render_table(action.get("inputs") or {}) + block = f"{START}\n\n{table}\n\n{END}" + + readme = README.read_text(encoding="utf-8") + pattern = re.escape(START) + r".*?" + re.escape(END) + if not re.search(pattern, readme, flags=re.DOTALL): + raise SystemExit("AUTO-DOC-INPUT markers not found in README.md") + + new = re.sub(pattern, lambda _: block, readme, flags=re.DOTALL) + README.write_text(new, encoding="utf-8", newline="\n") + + +if __name__ == "__main__": + main() diff --git a/generate-doc.sh b/generate-doc.sh new file mode 100755 index 0000000..feb77bb --- /dev/null +++ b/generate-doc.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euo pipefail + +command -v uv >/dev/null 2>&1 || { echo "uv is not installed: https://docs.astral.sh/uv/" >&2; exit 1; } + +exec uv run "$(dirname "$0")/generate-doc.py" diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..6865721 --- /dev/null +++ b/renovate.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:best-practices", + ":semanticCommitTypeAll(chore)" + ], + "prHourlyLimit": 10, + "rebaseWhen": "conflicted", + "minimumReleaseAge": "7 days", + "osvVulnerabilityAlerts": true, + "lockFileMaintenance": { "enabled": true }, + "packageRules": [ + { + "matchDatasources": ["pypi"], + "matchPackageNames": ["mergify-cli"], + "minimumReleaseAge": "2 days" + }, + { + "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.", + "matchDepNames": ["Mergifyio/setup-cli"], + "minimumReleaseAge": "0" + } + ], + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": [ + "/^action\\.yml$/" + ], + "matchStrings": [ + "# renovate: datasource=(?[a-z-]+) depName=(?[^\\s]+)(?: versioning=(?[a-z0-9-]+))?\\s+default:\\s*(?[^\\s]+)" + ] + }, + { + "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.", + "customType": "regex", + "managerFilePatterns": [ + "/^README\\.md$/" + ], + "matchStrings": [ + "\\| `mergify_cli_version` \\| string \\| \\w+ \\| `(?[^`]+)` \\|" + ], + "datasourceTemplate": "pypi", + "depNameTemplate": "mergify-cli" + }, + { + "description": "Bump the self-referenced action version pinned in the README usage example on each new setup-cli release.", + "customType": "regex", + "managerFilePatterns": [ + "/^README\\.md$/" + ], + "matchStrings": [ + "uses: Mergifyio/setup-cli@(?v\\d+)" + ], + "datasourceTemplate": "github-tags", + "depNameTemplate": "Mergifyio/setup-cli", + "versioningTemplate": "github-actions" + } + ] +}