From d177739340b62057c2ff7242cc11e0f64f6e94b0 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Thu, 21 May 2026 21:57:15 -0700 Subject: [PATCH 1/3] chore: security issue reporting and other templates Add instructions for how security vulnerabilities may be reported. Also add other issue templates so reports can be more organized and reporters know what information is needed in a report. --- .github/ISSUE_TEMPLATE/bug-report.yml | 64 +++++++++++++++++++ .../ISSUE_TEMPLATE/compatibility-report.yml | 59 +++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature-request.yml | 33 ++++++++++ .github/ISSUE_TEMPLATE/security.md | 14 ++++ SECURITY.md | 38 +++++++++++ 6 files changed, 213 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/compatibility-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/security.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..90c6aba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,64 @@ +name: Bug report +description: Report a reproducible problem with the SSH library. +title: "Bug: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a problem. Please include enough detail for maintainers to reproduce or reason about the issue. + - type: textarea + id: summary + attributes: + label: Summary + description: What went wrong? + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + description: Include a minimal code sample or command sequence when possible. + placeholder: | + 1. Configure client with ... + 2. Connect to ... + 3. Observe ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect to happen? + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What happened instead? Include stack traces or logs if available. + render: text + validations: + required: true + - type: input + id: version + attributes: + label: Library version + placeholder: "0.2.2-SNAPSHOT" + validations: + required: true + - type: input + id: runtime + attributes: + label: Runtime + description: Kotlin/JDK/Android version, if relevant. + placeholder: "Kotlin 2.3.21, JDK 21" + validations: + required: false + - type: textarea + id: context + attributes: + label: Additional context + description: Server implementation, algorithms, platform, or anything else useful. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/compatibility-report.yml b/.github/ISSUE_TEMPLATE/compatibility-report.yml new file mode 100644 index 0000000..e278fcd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/compatibility-report.yml @@ -0,0 +1,59 @@ +name: Compatibility report +description: Report interoperability with a specific SSH server, algorithm, or client workflow. +title: "Compatibility: " +labels: [compatibility] +body: + - type: input + id: server + attributes: + label: SSH server + description: Server implementation and version. + placeholder: "OpenSSH_9.9p2" + validations: + required: true + - type: input + id: library-version + attributes: + label: Library version + placeholder: "0.2.2-SNAPSHOT" + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - Connection setup + - Key exchange + - Host key verification + - Authentication + - Shell/session channel + - SFTP + - Port forwarding + - Agent forwarding + - Other + validations: + required: true + - type: textarea + id: algorithms + attributes: + label: Algorithms and configuration + description: Include negotiated algorithms, client config, auth methods, or server config if known. + render: text + validations: + required: false + - type: textarea + id: behavior + attributes: + label: Observed behavior + description: What works or fails? Include logs or packet-level details if available. + render: text + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect based on other clients, server docs, or protocol references? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ed83824 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/connectbot/cbssh/security/advisories/new + about: Please report vulnerabilities privately. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..f5a0245 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,33 @@ +name: Feature request +description: Suggest a new capability or API improvement. +title: "Feature: " +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What are you trying to do, and what is missing today? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: Describe the API, behavior, algorithm, or workflow you would like. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Workarounds, other libraries, or different designs you considered. + validations: + required: false + - type: textarea + id: context + attributes: + label: Additional context + description: Relevant RFCs, OpenSSH behavior, server compatibility, or examples. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/security.md b/.github/ISSUE_TEMPLATE/security.md new file mode 100644 index 0000000..ef31498 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security.md @@ -0,0 +1,14 @@ +--- +name: Security report +about: Report a vulnerability or sensitive security issue. +title: "Security: " +labels: security +--- + +Please do not open a public issue for vulnerabilities or sensitive security reports. + +Use GitHub's private vulnerability reporting for this repository: + +https://github.com/connectbot/cbssh/security/advisories/new + +Contact the maintainers privately if private reporting is unavailable. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..689fb23 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,38 @@ +# Security Policy + +## Reporting a Vulnerability + +Please do not report security vulnerabilities in public GitHub issues. + +Use GitHub's private vulnerability reporting for this repository: + +https://github.com/connectbot/cbssh/security/advisories/new + +If private vulnerability reporting is unavailable, contact the maintainers privately before publishing details. + +## What to Include + +When possible, include: + +- The affected library version or commit. +- A clear description of the vulnerability and impact. +- Steps to reproduce, proof-of-concept code, or relevant logs. +- Any known affected SSH servers, algorithms, authentication methods, or protocol messages. +- Whether the issue is already public or has been reported elsewhere. + +Please avoid including secrets, private keys, production credentials, or sensitive host details in reports. + +## Supported Versions + +Security fixes are generally provided for: + +- The current development line on `main`. +- Active maintenance branches named `release/`. + +Older versions may receive fixes when the impact is severe and a maintenance branch exists or can be reasonably created. + +## Disclosure Process + +Maintainers will review private reports and coordinate a fix before public disclosure when appropriate. Depending on severity and complexity, the fix may be released from `main`, an active `release/` branch, or both. + +Public advisories, release notes, and CVE requests will be handled after a fix is available or a coordinated disclosure date is reached. From 7153143f0747b13c72bbe7bd4cda72a94324e30f Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Thu, 21 May 2026 22:20:50 -0700 Subject: [PATCH 2/3] chore: add release workflow Use GitHub Actions to trigger the release workflow via issue. --- .github/ISSUE_TEMPLATE/release-branch.yml | 34 ++ .github/ISSUE_TEMPLATE/release.yml | 41 +++ .github/scripts/comment-issue-failure.sh | 34 ++ .github/scripts/create-release-branch.sh | 38 +++ .github/scripts/prepare-release.sh | 45 +++ .github/scripts/publish-release.sh | 110 +++++++ .github/scripts/release-common.sh | 127 ++++++++ .github/scripts/upsert-release-pr.sh | 42 +++ .github/tests/release-scripts.bats | 297 ++++++++++++++++++ .github/workflows/ci.yml | 70 ++++- .github/workflows/codeql.yml | 4 +- .github/workflows/dependency-review.yml | 20 ++ .../dependency-submission-submit-pr.yml | 22 ++ .github/workflows/dependency-submission.yml | 80 +++++ .github/workflows/release-branch.yml | 38 +++ .github/workflows/release-prepare.yml | 62 ++++ .github/workflows/release-publish.yml | 53 ++++ build.gradle.kts | 34 +- mise.toml | 6 + 19 files changed, 1148 insertions(+), 9 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/release-branch.yml create mode 100644 .github/ISSUE_TEMPLATE/release.yml create mode 100644 .github/scripts/comment-issue-failure.sh create mode 100644 .github/scripts/create-release-branch.sh create mode 100644 .github/scripts/prepare-release.sh create mode 100644 .github/scripts/publish-release.sh create mode 100644 .github/scripts/release-common.sh create mode 100644 .github/scripts/upsert-release-pr.sh create mode 100644 .github/tests/release-scripts.bats create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/dependency-submission-submit-pr.yml create mode 100644 .github/workflows/dependency-submission.yml create mode 100644 .github/workflows/release-branch.yml create mode 100644 .github/workflows/release-prepare.yml create mode 100644 .github/workflows/release-publish.yml create mode 100644 mise.toml diff --git a/.github/ISSUE_TEMPLATE/release-branch.yml b/.github/ISSUE_TEMPLATE/release-branch.yml new file mode 100644 index 0000000..d6be6de --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release-branch.yml @@ -0,0 +1,34 @@ +name: Release branch +description: Create a maintenance release branch. +title: "Create release branch " +labels: [] +body: + - type: markdown + attributes: + value: | + This release branch automation is for maintainers only. Submissions from non-maintainers will be rejected by the workflow. + - type: input + id: maintenanceBranch + attributes: + label: Maintenance branch + description: Branch to create for maintenance releases. + placeholder: "release/1.1" + validations: + required: true + - type: input + id: sourceBranch + attributes: + label: Source branch + description: Branch to cut from. + placeholder: "main" + value: "main" + validations: + required: true + - type: input + id: sourceRef + attributes: + label: Source ref + description: Optional exact ref or SHA. Leave blank to use the source branch tip. + placeholder: "main" + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml new file mode 100644 index 0000000..53ba39d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -0,0 +1,41 @@ +name: Release +description: Prepare and publish a library release. +title: "Release " +labels: [] +body: + - type: markdown + attributes: + value: | + This release automation is for maintainers only. Submissions from non-maintainers will be rejected by the workflow. + - type: input + id: releaseVersion + attributes: + label: Release version + description: Version to publish, without the v tag prefix. + placeholder: "1.0.4" + validations: + required: true + - type: input + id: nextVersion + attributes: + label: Next version + description: Next development version after the release. + placeholder: "1.0.5-SNAPSHOT" + validations: + required: true + - type: input + id: targetBranch + attributes: + label: Target branch + description: Branch to release from and fast-forward on publish. + placeholder: "main" + value: "main" + validations: + required: true + - type: textarea + id: releaseNotes + attributes: + label: Release notes + description: Optional text to include in the annotated tag message. + validations: + required: false diff --git a/.github/scripts/comment-issue-failure.sh b/.github/scripts/comment-issue-failure.sh new file mode 100644 index 0000000..8715ae4 --- /dev/null +++ b/.github/scripts/comment-issue-failure.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +log_file="$1" +workflow_name="$2" + +body_file="$(mktemp)" +redacted_log="$(mktemp)" +if [[ -s "${log_file}" ]]; then + cp "${log_file}" "${redacted_log}" + if [[ -n "${PUSH_TOKEN:-}" ]]; then + perl -0pi -e 'BEGIN { $s = $ENV{PUSH_TOKEN} // "" } if (length $s) { s/\Q$s\E//g }' "${redacted_log}" + fi + if [[ -n "${GH_TOKEN:-}" ]]; then + perl -0pi -e 'BEGIN { $s = $ENV{GH_TOKEN} // "" } if (length $s) { s/\Q$s\E//g }' "${redacted_log}" + fi +fi + +{ + echo "${workflow_name} failed." + echo + echo '```text' + if [[ -s "${redacted_log}" ]]; then + tail -n 80 "${redacted_log}" + else + echo "No script output was captured." + fi + echo '```' + echo + echo "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" +} >"${body_file}" + +gh issue comment "${ISSUE_NUMBER}" --body-file "${body_file}" diff --git a/.github/scripts/create-release-branch.sh b/.github/scripts/create-release-branch.sh new file mode 100644 index 0000000..b50404f --- /dev/null +++ b/.github/scripts/create-release-branch.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=.github/scripts/release-common.sh +source "${script_dir}/release-common.sh" + +require_maintainer + +maintenance_branch="$(extract_issue_field_line "Maintenance branch")" +source_branch="$(extract_issue_field_line "Source branch")" +source_ref="$(extract_issue_field_line "Source ref" || true)" + +validate_maintenance_branch "${maintenance_branch}" +validate_release_target_branch "${source_branch}" +if [[ -z "${source_ref}" ]]; then + source_ref="origin/${source_branch}" +elif [[ "${source_ref}" == "${source_branch}" ]]; then + source_ref="origin/${source_branch}" +fi +if [[ "${source_ref}" == -* ]]; then + echo "Source ref must not start with '-'." + exit 1 +fi + +require_missing_remote_branch "${maintenance_branch}" +require_remote_branch "${source_branch}" + +configure_git_credentials +git fetch origin "+refs/heads/${source_branch}:refs/remotes/origin/${source_branch}" --tags +git rev-parse --verify --end-of-options "${source_ref}^{commit}" >/dev/null +if ! git merge-base --is-ancestor -- "${source_ref}^{commit}" "origin/${source_branch}"; then + echo "Source ref ${source_ref} is not reachable from origin/${source_branch}." + exit 1 +fi +git push origin "${source_ref}^{commit}:refs/heads/${maintenance_branch}" +gh issue comment "${ISSUE_NUMBER}" --body "Created ${maintenance_branch} from ${source_ref}." diff --git a/.github/scripts/prepare-release.sh b/.github/scripts/prepare-release.sh new file mode 100644 index 0000000..5005378 --- /dev/null +++ b/.github/scripts/prepare-release.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=.github/scripts/release-common.sh +source "${script_dir}/release-common.sh" + +require_maintainer + +release_version="$(extract_issue_field_line "Release version")" +next_version="$(extract_issue_field_line "Next version")" +target_branch="$(extract_issue_field_line "Target branch")" + +validate_release_version "${release_version}" +validate_next_version "${next_version}" +validate_release_target_branch "${target_branch}" + +tag_name="v${release_version}" +work_branch="release-work/${release_version}" + +require_missing_remote_tag "${tag_name}" +require_remote_branch "${target_branch}" +require_missing_remote_branch "${work_branch}" + +configure_git_author +configure_git_credentials +git fetch origin "+refs/heads/${target_branch}:refs/remotes/origin/${target_branch}" --tags +git checkout -B "${work_branch}" "origin/${target_branch}" + +./gradlew release \ + -Pcbssh.release.noPush=true \ + -Prelease.useAutomaticVersion=true \ + -Prelease.releaseVersion="${release_version}" \ + -Prelease.newVersion="${next_version}" + +git push origin "HEAD:refs/heads/${work_branch}" + +{ + echo "release_version=${release_version}" + echo "next_version=${next_version}" + echo "target_branch=${target_branch}" + echo "work_branch=${work_branch}" + echo "tag_name=${tag_name}" +} >>"${GITHUB_OUTPUT}" diff --git a/.github/scripts/publish-release.sh b/.github/scripts/publish-release.sh new file mode 100644 index 0000000..dbaba03 --- /dev/null +++ b/.github/scripts/publish-release.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=.github/scripts/release-common.sh +source "${script_dir}/release-common.sh" + +require_maintainer + +release_version="$(extract_issue_field_line "Release version")" +next_version="$(extract_issue_field_line "Next version")" +target_branch="$(extract_issue_field_line "Target branch")" +release_notes="$(extract_issue_field "Release notes" || true)" + +validate_release_version "${release_version}" +validate_next_version "${next_version}" +validate_release_target_branch "${target_branch}" + +tag_name="v${release_version}" +work_branch="release-work/${release_version}" + +require_missing_remote_tag "${tag_name}" + +pr_number="$(gh pr list \ + --head "${work_branch}" \ + --base "${target_branch}" \ + --state open \ + --json number \ + --jq '.[0].number // empty')" + +if [[ -z "${pr_number}" ]]; then + echo "No open release PR found for ${work_branch} into ${target_branch}." + exit 1 +fi + +pr_data="$(gh pr view "${pr_number}" --json baseRefName,headRefName,headRefOid,mergeable,mergeStateStatus,reviewDecision)" +actual_base="$(jq -r '.baseRefName' <<<"${pr_data}")" +actual_head="$(jq -r '.headRefName' <<<"${pr_data}")" +head_ref_oid="$(jq -r '.headRefOid' <<<"${pr_data}")" +mergeable="$(jq -r '.mergeable' <<<"${pr_data}")" +merge_state="$(jq -r '.mergeStateStatus' <<<"${pr_data}")" +review_decision="$(jq -r '.reviewDecision' <<<"${pr_data}")" + +if [[ "${actual_base}" != "${target_branch}" || "${actual_head}" != "${work_branch}" ]]; then + echo "Release PR does not match the issue target branch and work branch." + exit 1 +fi +if [[ "${mergeable}" != "MERGEABLE" ]]; then + echo "Release PR must be mergeable before publishing. Current mergeable state: ${mergeable}." + exit 1 +fi +if [[ "${merge_state}" != "CLEAN" ]]; then + echo "Release PR must have a clean merge state before publishing. Current merge state: ${merge_state}." + exit 1 +fi +if [[ "${review_decision}" != "APPROVED" ]]; then + echo "Release PR must be approved before publishing. Current review decision: ${review_decision}." + exit 1 +fi + +gh pr checks "${pr_number}" --required --fail-fast + +configure_git_author +configure_git_credentials +git fetch origin \ + "+refs/heads/${target_branch}:refs/remotes/origin/${target_branch}" \ + "+refs/heads/${work_branch}:refs/remotes/origin/${work_branch}" \ + --tags + +if [[ "$(git rev-parse "origin/${work_branch}")" != "${head_ref_oid}" ]]; then + echo "Release branch changed after PR checks were inspected." + exit 1 +fi + +release_commit="" +while read -r commit; do + if git show "${commit}:gradle.properties" | grep -q "^version=${release_version}$"; then + release_commit="${commit}" + break + fi +done < <(git rev-list --reverse "origin/${target_branch}..origin/${work_branch}") + +if [[ -z "${release_commit}" ]]; then + echo "Could not find release commit with version=${release_version}." + exit 1 +fi + +if ! git show "origin/${work_branch}:gradle.properties" | grep -q "^version=${next_version}$"; then + echo "Release branch tip does not contain version=${next_version}." + exit 1 +fi + +tag_message="$(mktemp)" +{ + echo "Release ${tag_name}" + if [[ -n "${release_notes}" ]]; then + echo + echo "${release_notes}" + fi +} >"${tag_message}" + +git tag -a "${tag_name}" "${release_commit}" -F "${tag_message}" +if [[ "$(git cat-file -t "${tag_name}")" != "tag" ]]; then + echo "${tag_name} is not an annotated tag." + exit 1 +fi + +git push --atomic --follow-tags origin "refs/remotes/origin/${work_branch}:refs/heads/${target_branch}" +gh issue comment "${ISSUE_NUMBER}" --body "Published ${tag_name} to ${target_branch}." diff --git a/.github/scripts/release-common.sh b/.github/scripts/release-common.sh new file mode 100644 index 0000000..749ef0d --- /dev/null +++ b/.github/scripts/release-common.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +set -euo pipefail + +require_maintainer() { + local actor="${RELEASE_ACTOR:-}" + local permission + + if [[ -z "${actor}" ]]; then + echo "Release automation requires RELEASE_ACTOR to be set." + exit 1 + fi + + permission="$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${actor}/permission" --jq '.permission')" + + case "${permission}" in + admin|maintain) ;; + *) + echo "Release automation must be started by a maintainer with maintain or admin access. ${actor} has ${permission} permission." + exit 1 + ;; + esac +} + +extract_issue_field() { + local heading="$1" + awk -v heading="$heading" ' + $0 == "### " heading { found = 1; next } + found && /^### / { exit } + found { print } + ' <<<"${ISSUE_BODY:-}" | + sed '//d' | + awk ' + { lines[NR] = $0 } + END { + start = 1 + end = NR + while (start <= end && lines[start] ~ /^[[:space:]]*$/) { + start++ + } + while (end >= start && lines[end] ~ /^[[:space:]]*$/) { + end-- + } + for (i = start; i <= end; i++) { + print lines[i] + } + } + ' +} + +extract_issue_field_line() { + extract_issue_field "$1" | head -n 1 | tr -d '[:space:]' +} + +validate_release_version() { + local version="$1" + if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Release version must look like 1.2.3." + exit 1 + fi +} + +validate_next_version() { + local version="$1" + if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-SNAPSHOT$ ]]; then + echo "Next version must look like 1.2.4-SNAPSHOT." + exit 1 + fi +} + +validate_release_target_branch() { + local branch="$1" + if [[ ! "${branch}" =~ ^(main|release/[0-9]+\.[0-9]+)$ ]]; then + echo "Target branch must be main or release/." + exit 1 + fi +} + +validate_maintenance_branch() { + local branch="$1" + if [[ ! "${branch}" =~ ^release/[0-9]+\.[0-9]+$ ]]; then + echo "Maintenance branch must look like release/1.1." + exit 1 + fi +} + +require_remote_branch() { + local branch="$1" + if ! git ls-remote --exit-code --heads origin "refs/heads/${branch}" >/dev/null 2>&1; then + echo "Branch ${branch} does not exist." + exit 1 + fi +} + +require_missing_remote_branch() { + local branch="$1" + if git ls-remote --exit-code --heads origin "refs/heads/${branch}" >/dev/null 2>&1; then + echo "Branch ${branch} already exists." + exit 1 + fi +} + +require_missing_remote_tag() { + local tag="$1" + if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then + echo "Tag ${tag} already exists." + exit 1 + fi +} + +configure_git_author() { + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" +} + +configure_git_credentials() { + if [[ -z "${PUSH_TOKEN:-}" ]]; then + echo "PUSH_TOKEN is required for authenticated git pushes." + exit 1 + fi + + git config --global credential.helper store + { + printf "https://x-access-token:%s@github.com\n" "${PUSH_TOKEN}" + } >"${HOME}/.git-credentials" + chmod 0600 "${HOME}/.git-credentials" +} diff --git a/.github/scripts/upsert-release-pr.sh b/.github/scripts/upsert-release-pr.sh new file mode 100644 index 0000000..3245fa9 --- /dev/null +++ b/.github/scripts/upsert-release-pr.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +release_version="$1" +next_version="$2" +target_branch="$3" +work_branch="$4" +tag_name="$5" + +body_file="$(mktemp)" +cat >"${body_file}" <"${WORKDIR}/gradlew" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +{ + printf 'gradlew' + printf ' %q' "$@" + printf '\n' +} >>"${LOG_FILE}" +STUB + chmod +x "${WORKDIR}/gradlew" +} + +make_git_stub() { + cat >"${BIN_DIR}/git" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +{ + printf 'git' + printf ' %q' "$@" + printf '\n' +} >>"${LOG_FILE}" + +case "$1" in + ls-remote) + if [[ "$*" == *"refs/tags/v9.9.9"* ]]; then + exit 0 + fi + if [[ "$*" == *"refs/tags/"* || "$*" == *"refs/heads/missing"* || "$*" == *"refs/heads/release/1.1"* ]]; then + exit 2 + fi + if [[ "$*" == *"refs/heads/release-work/9.9.8"* ]]; then + exit 0 + fi + if [[ "$*" == *"refs/heads/release-work/"* ]]; then + exit 2 + fi + exit 0 + ;; + show) + case "$2" in + release-commit:gradle.properties) + echo "version=1.2.3" + ;; + refs/remotes/origin/release-work/1.2.3:gradle.properties|origin/release-work/1.2.3:gradle.properties) + echo "version=1.2.4-SNAPSHOT" + ;; + *) + echo "version=0.0.0-SNAPSHOT" + ;; + esac + ;; + rev-list) + echo "release-commit" + echo "next-commit" + ;; + cat-file) + echo "tag" + ;; + rev-parse) + if [[ "${2:-}" == "origin/release-work/1.2.3" ]]; then + echo "head-sha" + fi + exit 0 + ;; + merge-base) + exit 0 + ;; +esac +STUB + chmod +x "${BIN_DIR}/git" +} + +make_gh_stub() { + cat >"${BIN_DIR}/gh" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +{ + printf 'gh' + printf ' %q' "$@" + printf '\n' +} >>"${LOG_FILE}" + +if [[ "$1" == "api" ]]; then + echo "maintain" +elif [[ "$1 $2" == "pr list" ]]; then + echo "77" +elif [[ "$1 $2" == "pr view" ]]; then + echo '{"baseRefName":"main","headRefName":"release-work/1.2.3","headRefOid":"head-sha","mergeable":"MERGEABLE","mergeStateStatus":"CLEAN","reviewDecision":"APPROVED"}' +fi +STUB + chmod +x "${BIN_DIR}/gh" +} + +release_issue_body() { + cat <<'EOF' +### Release version + +1.2.3 + +### Next version + +1.2.4-SNAPSHOT + +### Target branch + +main + +### Release notes + +Security fixes and release automation. + +Second paragraph stays separate. +EOF +} + +branch_issue_body() { + cat <<'EOF' +### Maintenance branch + +release/1.1 + +### Source branch + +main + +### Source ref + +EOF +} + +@test "release-common extracts issue form fields" { + export ISSUE_BODY + ISSUE_BODY="$(release_issue_body)" + + run bash -c 'source .github/scripts/release-common.sh; extract_issue_field_line "Release version"' + + [ "$status" -eq 0 ] + [ "$output" = "1.2.3" ] +} + +@test "release-common preserves internal blank lines in multi-line fields" { + export ISSUE_BODY + ISSUE_BODY="$(release_issue_body)" + + run bash -c 'source .github/scripts/release-common.sh; extract_issue_field "Release notes"' + + [ "$status" -eq 0 ] + [[ "$output" == $'Security fixes and release automation.\n\nSecond paragraph stays separate.' ]] +} + +@test "prepare-release creates a release-work branch with netrelease no-push mode" { + export ISSUE_BODY + ISSUE_BODY="$(release_issue_body)" + + run bash .github/scripts/prepare-release.sh + + [ "$status" -eq 0 ] + grep -F "gh api repos/connectbot/cbssh/collaborators/maintainer/permission --jq .permission" "${LOG_FILE}" + grep -F "git config --global credential.helper store" "${LOG_FILE}" + grep -F "git checkout -B release-work/1.2.3 origin/main" "${LOG_FILE}" + grep -F "gradlew release -Pcbssh.release.noPush=true -Prelease.useAutomaticVersion=true -Prelease.releaseVersion=1.2.3 -Prelease.newVersion=1.2.4-SNAPSHOT" "${LOG_FILE}" + grep -F "git push origin HEAD:refs/heads/release-work/1.2.3" "${LOG_FILE}" + grep -F "tag_name=v1.2.3" "${GITHUB_OUTPUT}" +} + +@test "prepare-release rejects an existing v-prefixed tag" { + export ISSUE_BODY + ISSUE_BODY="$(release_issue_body | sed 's/1.2.3/9.9.9/')" + + run bash .github/scripts/prepare-release.sh + + [ "$status" -ne 0 ] + [[ "$output" == *"Tag v9.9.9 already exists."* ]] +} + +@test "prepare-release rejects an existing release-work branch" { + export ISSUE_BODY + ISSUE_BODY="$(release_issue_body | sed 's/1.2.3/9.9.8/')" + + run bash .github/scripts/prepare-release.sh + + [ "$status" -ne 0 ] + [[ "$output" == *"Branch release-work/9.9.8 already exists."* ]] +} + +@test "publish-release creates an annotated v tag and atomic fast-forward push" { + export ISSUE_BODY + ISSUE_BODY="$(release_issue_body)" + + run bash .github/scripts/publish-release.sh + + [ "$status" -eq 0 ] + grep -F "gh pr checks 77 --required --fail-fast" "${LOG_FILE}" + grep -F "git config --global credential.helper store" "${LOG_FILE}" + grep -F "git tag -a v1.2.3 release-commit -F" "${LOG_FILE}" + grep -F "git push --atomic --follow-tags origin refs/remotes/origin/release-work/1.2.3:refs/heads/main" "${LOG_FILE}" + grep -F "gh issue comment 123 --body Published\ v1.2.3\ to\ main." "${LOG_FILE}" + grep -F "https://x-access-token:push-token@github.com" "${HOME}/.git-credentials" +} + +@test "create-release-branch cuts from the remote source branch tip by default" { + export ISSUE_BODY + ISSUE_BODY="$(branch_issue_body)" + + run bash .github/scripts/create-release-branch.sh + + [ "$status" -eq 0 ] + grep -F "git config --global credential.helper store" "${LOG_FILE}" + grep -F "git fetch origin +refs/heads/main:refs/remotes/origin/main --tags" "${LOG_FILE}" + grep -F "git rev-parse --verify --end-of-options origin/main\\^\\{commit\\}" "${LOG_FILE}" + grep -F "git merge-base --is-ancestor -- origin/main\\^\\{commit\\} origin/main" "${LOG_FILE}" + grep -F "git push origin origin/main\\^\\{commit\\}:refs/heads/release/1.1" "${LOG_FILE}" +} + +@test "create-release-branch rejects option-like source refs" { + export ISSUE_BODY + ISSUE_BODY="$(cat <<'EOF' +### Maintenance branch + +release/1.1 + +### Source branch + +main + +### Source ref + +--help +EOF +)" + + run bash .github/scripts/create-release-branch.sh + + [ "$status" -ne 0 ] + [[ "$output" == *"Source ref must not start with '-'."* ]] +} + +@test "create-release-branch rejects invalid maintenance branch names" { + export ISSUE_BODY + ISSUE_BODY="$(branch_issue_body | sed 's#release/1.1#main#')" + + run bash .github/scripts/create-release-branch.sh + + [ "$status" -ne 0 ] + [[ "$output" == *"Maintenance branch must look like release/1.1."* ]] +} + +@test "comment-issue-failure posts captured output and run URL" { + failure_log="${BATS_TEST_TMPDIR}/failure.log" + cat >"${failure_log}" <<'EOF' +Release version must look like 1.2.3. +push-token +EOF + + run bash .github/scripts/comment-issue-failure.sh "${failure_log}" "Prepare release" + + [ "$status" -eq 0 ] + grep -F "gh issue comment 123 --body-file" "${LOG_FILE}" + body_file="$(awk '/^gh issue comment 123 --body-file / { print $6 }' "${LOG_FILE}")" + grep -F "Prepare release failed." "${body_file}" + grep -F "Release version must look like 1.2.3." "${body_file}" + grep -F "" "${body_file}" + ! grep -F "push-token" "${body_file}" + grep -F "Run: https://github.com/connectbot/cbssh/actions/runs/456" "${body_file}" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b4c7af..3a85348 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,16 +2,42 @@ name: Continuous Integration on: push: - branches: ['*'] + branches: ['**'] + tags: ['v*.*.*'] pull_request: - branches: [main] + branches: [main, 'release/**'] schedule: - cron: '30 5 * * *' permissions: - contents: write + contents: read jobs: + workflow-tests: + name: Workflow scripts + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup mise + uses: jdx/mise-action@be3be2260bc02bc3fbf94c5e2fed8b7964baf074 # v3.4.0 + with: + install: true + install_args: actionlint bats jq shellcheck + + - name: Check workflow syntax + run: mise exec -- actionlint + + - name: Check shell scripts + run: mise exec -- shellcheck -x .github/scripts/*.sh + + - name: Test release scripts + run: mise exec -- bats --print-output-on-failure .github/tests/release-scripts.bats + build: name: Build and test runs-on: ubuntu-latest @@ -36,7 +62,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - dependency-graph: generate-and-submit + dependency-graph: disabled validate-wrappers: true - name: Configure Docker mirror @@ -47,8 +73,40 @@ jobs: - name: Build with Gradle run: ./gradlew build koverXmlReport --info -PjdkVersion=${{ matrix.java }} - - name: Upload to Sonatype - if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.java == '17' + - name: Read project version + id: project-version + if: matrix.java == '17' + run: | + version="$(sed -n 's/^version=//p' gradle.properties)" + echo "version=${version}" >> "${GITHUB_OUTPUT}" + + - name: Verify release tag + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && matrix.java == '17' + run: | + test "$(git cat-file -t "${GITHUB_REF_NAME}")" = "tag" + git fetch origin \ + "+refs/heads/main:refs/remotes/origin/main" \ + "+refs/heads/release/*:refs/remotes/origin/release/*" + tag_commit="$(git rev-list -n 1 "${GITHUB_REF_NAME}")" + git branch -r --contains "${tag_commit}" | grep -E 'origin/(main|release/[0-9]+\.[0-9]+)$' + + - name: Upload snapshot to Sonatype + if: > + github.event_name == 'push' && + matrix.java == '17' && + endsWith(steps.project-version.outputs.version, '-SNAPSHOT') && + (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) + run: | + ./gradlew publishToMavenCentral --no-configuration-cache -PmavenCentralUsername="${ORG_GRADLE_PROJECT_mavenCentralUsername}" -PmavenCentralPassword="${ORG_GRADLE_PROJECT_mavenCentralPassword}" + env: + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.MAVEN_GPG_KEYID }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} + + - name: Upload release to Sonatype + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && matrix.java == '17' run: | ./gradlew publishToMavenCentral --no-configuration-cache -PmavenCentralUsername="${ORG_GRADLE_PROJECT_mavenCentralUsername}" -PmavenCentralPassword="${ORG_GRADLE_PROJECT_mavenCentralPassword}" env: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9dae418..d9a8d9d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,9 +2,9 @@ name: "CodeQL Advanced" on: push: - branches: [ "main" ] + branches: [ "main", "release/**" ] pull_request: - branches: [ "main" ] + branches: [ "main", "release/**" ] schedule: - cron: '22 22 * * 1' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..5d42ceb --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Dependency Review +on: + pull_request: + branches: [main, 'release/**'] + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency review + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Dependency review + uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3 + with: + retry-on-snapshot-warnings: true + retry-on-snapshot-warnings-timeout: 600 diff --git a/.github/workflows/dependency-submission-submit-pr.yml b/.github/workflows/dependency-submission-submit-pr.yml new file mode 100644 index 0000000..0198b4d --- /dev/null +++ b/.github/workflows/dependency-submission-submit-pr.yml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Submit pull request dependency graph +on: + workflow_run: + workflows: ['Dependency Submission'] + types: [completed] + +permissions: + actions: read + contents: write + +jobs: + submit-dependency-graph: + name: Submit pull request dependency graph + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Download and submit dependency graph + uses: gradle/actions/dependency-submission@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + dependency-graph: download-and-submit diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml new file mode 100644 index 0000000..6932d85 --- /dev/null +++ b/.github/workflows/dependency-submission.yml @@ -0,0 +1,80 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Dependency Submission +on: + push: + branches: [main, 'release/**'] + pull_request: + branches: [main, 'release/**'] + +jobs: + submit-dependency-graph: + name: Submit push dependency graph + if: github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: 'zulu' + java-version: '17' + + - name: Generate and submit dependency graph + uses: gradle/actions/dependency-submission@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + dependency-graph: generate-and-submit + + submit-pull-request-dependency-graph: + name: Submit pull request dependency graph + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: 'zulu' + java-version: '17' + + - name: Generate and submit dependency graph + uses: gradle/actions/dependency-submission@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + dependency-graph: generate-and-submit + + generate-fork-dependency-graph: + name: Generate fork dependency graph + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: 'zulu' + java-version: '17' + + - name: Generate dependency graph + uses: gradle/actions/dependency-submission@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + dependency-graph: generate-and-upload diff --git a/.github/workflows/release-branch.yml b/.github/workflows/release-branch.yml new file mode 100644 index 0000000..7b68fe1 --- /dev/null +++ b/.github/workflows/release-branch.yml @@ -0,0 +1,38 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Create release branch +on: + issues: + types: [labeled] + +permissions: + contents: write + issues: write + +jobs: + branch: + name: Create maintenance branch + if: github.event.label.name == 'release:branch' + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + GH_TOKEN: ${{ github.token }} + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + PUSH_TOKEN: ${{ github.token }} + RELEASE_ACTOR: ${{ github.event.sender.login }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + + - name: Create branch + run: | + set -o pipefail + bash .github/scripts/create-release-branch.sh 2>&1 | tee "${RUNNER_TEMP}/release-branch.log" + + - name: Comment on failure + if: failure() + run: bash .github/scripts/comment-issue-failure.sh "${RUNNER_TEMP}/release-branch.log" "Create release branch" diff --git a/.github/workflows/release-prepare.yml b/.github/workflows/release-prepare.yml new file mode 100644 index 0000000..97b5e75 --- /dev/null +++ b/.github/workflows/release-prepare.yml @@ -0,0 +1,62 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Prepare release +on: + issues: + types: [labeled] + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + prepare: + name: Prepare release pull request + if: github.event.label.name == 'release:prepare' + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + GH_TOKEN: ${{ github.token }} + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + PUSH_TOKEN: ${{ github.token }} + RELEASE_ACTOR: ${{ github.event.sender.login }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + validate-wrappers: true + + - name: Prepare release branch + id: release + run: | + set -o pipefail + bash .github/scripts/prepare-release.sh 2>&1 | tee "${RUNNER_TEMP}/release-prepare.log" + + - name: Open or update release pull request + run: | + set -o pipefail + bash .github/scripts/upsert-release-pr.sh \ + "${{ steps.release.outputs.release_version }}" \ + "${{ steps.release.outputs.next_version }}" \ + "${{ steps.release.outputs.target_branch }}" \ + "${{ steps.release.outputs.work_branch }}" \ + "${{ steps.release.outputs.tag_name }}" \ + 2>&1 | tee -a "${RUNNER_TEMP}/release-prepare.log" + + - name: Comment on failure + if: failure() + run: bash .github/scripts/comment-issue-failure.sh "${RUNNER_TEMP}/release-prepare.log" "Prepare release" diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000..519541c --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,53 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Publish release +on: + issues: + types: [labeled] + +permissions: + contents: read + issues: write + pull-requests: read + checks: read + statuses: read + +jobs: + publish: + name: Publish release tag and branch + if: github.event.label.name == 'release:publish' + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + GH_TOKEN: ${{ github.token }} + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + RELEASE_ACTOR: ${{ github.event.sender.login }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + + - name: Create release app token + id: release-app-token + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + with: + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + + - name: Publish release + env: + PUSH_TOKEN: ${{ steps.release-app-token.outputs.token }} + run: | + set -o pipefail + bash .github/scripts/publish-release.sh 2>&1 | tee "${RUNNER_TEMP}/release-publish.log" + + - name: Comment on failure + if: failure() + env: + PUSH_TOKEN: ${{ steps.release-app-token.outputs.token }} + run: bash .github/scripts/comment-issue-failure.sh "${RUNNER_TEMP}/release-publish.log" "Publish release" diff --git a/build.gradle.kts b/build.gradle.kts index 2bd2083..771b1e8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,8 @@ * limitations under the License. */ +import net.researchgate.release.ReleaseExtension + plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.dokka) apply false @@ -39,6 +41,22 @@ dependencies { kover(project(":sshlib")) } +configure { + tagTemplate.set("v\${version}") + preTagCommitMessage.set("chore(release): ") + tagCommitMessage.set("Release ") + newVersionCommitMessage.set("chore(release): start ") + buildTasks.set(listOf("build")) + + git { + requireBranch.set("^(main|release/.+|release-work/.+)$") + commitVersionFileOnly.set(true) + if (providers.gradleProperty("cbssh.release.noPush").isPresent) { + pushToRemote.set(false) + } + } +} + sonar { properties { property("sonar.projectName", "ConnectBot SSH Library") @@ -67,8 +85,22 @@ sonar { } } +val spotlessRatchetRef = providers.environmentVariable("GITHUB_BASE_REF") + .map { "origin/$it" } + .orElse( + providers.environmentVariable("GITHUB_REF_NAME") + .map { refName -> + if (refName.startsWith("release/")) { + "origin/$refName" + } else { + "origin/main" + } + }, + ) + .getOrElse("origin/main") + spotless { - ratchetFrom = "origin/main" + ratchetFrom = spotlessRatchetRef kotlin { target("**/src/**/*.kt") diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..6221398 --- /dev/null +++ b/mise.toml @@ -0,0 +1,6 @@ +[tools] +actionlint = "1.7.12" +bats = "1.12.0" +java = "temurin-25.0.3+9.0.LTS" +jq = "1.8.1" +shellcheck = "0.11.0" From 2f0413988809d66a90a271e50eeaa7ca359718d6 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Thu, 21 May 2026 22:47:32 -0700 Subject: [PATCH 3/3] chore: update rootProject name --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 2ec4a15..34d57c1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "ssh-proto" +rootProject.name = "cbssh" include(":sshlib") include(":testapp")