diff --git a/.github/workflows/prepare-release-3.yml b/.github/workflows/prepare-release-3.yml new file mode 100644 index 00000000000..f97080f26fc --- /dev/null +++ b/.github/workflows/prepare-release-3.yml @@ -0,0 +1,116 @@ +name: Prepare Release 3.0 + +# Manual pre-release workflow for creating the prepare PR on branch 3.0.0. +on: + workflow_dispatch: + inputs: + codegen_version: + description: 'Release version, for example 3.0.80. Leave empty to derive from current SNAPSHOT.' + required: false + type: string + next_codegen_snapshot_version: + description: 'Next development version, for example 3.0.81-SNAPSHOT. Leave empty to increment codegen_version.' + required: false + type: string + release_generators: + description: 'Prepare for releasing swagger-codegen-generators with codegen' + required: true + default: 'false' + type: choice + options: + - 'false' + - 'true' + generators_version: + description: 'Generator release version when release_generators=true, for example 1.0.61' + required: false + type: string + previous_generators_version: + description: 'Optional bootstrap generator version for circular dependency validation' + required: false + type: string +permissions: + contents: write + pull-requests: write + +jobs: + prepare: + # Resolves release versions, applies file updates, validates build, then opens PR. + runs-on: ubuntu-latest + env: + CODEGEN_VERSION: ${{ inputs.codegen_version }} + NEXT_CODEGEN_SNAPSHOT_VERSION: ${{ inputs.next_codegen_snapshot_version }} + RELEASE_GENERATORS: ${{ inputs.release_generators }} + GENERATORS_VERSION: ${{ inputs.generators_version }} + PREVIOUS_GENERATORS_VERSION: ${{ inputs.previous_generators_version }} + steps: + - name: Checkout swagger-codegen 3.0.0 + uses: actions/checkout@v6 + with: + ref: 3.0.0 + fetch-depth: 0 + + - uses: actions/create-github-app-token@v3 + id: generate-token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Set up Java and Maven + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: temurin + cache: maven + + - name: Add Central Portal snapshot repository + uses: s4u/maven-settings-action@v4.0.0 + with: + repositories: '[{"id":"central-portal-snapshots","name":"Sonatype Central Portal snapshots","url":"https://central.sonatype.com/repository/maven-snapshots/","releases":{"enabled":false},"snapshots":{"enabled":true}}]' + servers: '[{"id":"central","username":"${{ secrets.MAVEN_CENTRAL_USERNAME }}","password":"${{ secrets.MAVEN_CENTRAL_PASSWORD }}"}]' + + - name: Validate bootstrap generator snapshot + if: inputs.previous_generators_version != '' + run: | + # Optional safeguard for circular dependency bootstrap inputs. + source CI/release/common.sh + require_release_or_snapshot_version "previous_generators_version" "${PREVIOUS_GENERATORS_VERSION}" + if [[ "${PREVIOUS_GENERATORS_VERSION}" =~ SNAPSHOT$ ]]; then + assert_snapshot_metadata_exists "${GENERATORS_ARTIFACT}" "${PREVIOUS_GENERATORS_VERSION}" + elif ! release_artifact_exists "${GENERATORS_ARTIFACT}" "${PREVIOUS_GENERATORS_VERSION}"; then + fail "previous_generators_version ${PREVIOUS_GENERATORS_VERSION} does not exist in Maven Central" + fi + + - name: Prepare release file changes + id: prepare-release + # Performs version bump to release and updates docs/poms/openapi. + run: bash CI/release/prepare-codegen-release.sh + + - name: Build release candidate + # Build with the resolved generators version to catch dependency issues early. + run: | + mvn -B -U clean install -Pdocker \ + -Dswagger-codegen-generators-version="${BUILD_GENERATORS_VERSION}" \ + -DJETTY_TEST_HTTP_PORT=8090 \ + -DJETTY_TEST_STOP_PORT=8089 + + - name: Print generator repo follow-up + if: inputs.release_generators == 'true' + run: | + # Generators repo changes are intentionally handled in a separate repository PR. + echo "::notice::Open a separate PR in swagger-api/swagger-codegen-generators master setting version ${GENERATORS_VERSION} and a usable swagger-codegen-version. This repository workflow does not push cross-repository generator changes." + + - name: Create prepare release pull request + # Opens PR with all prepare-release changes targeting 3.0.0. + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ steps.generate-token.outputs.token }} + commit-message: prepare release ${{ steps.prepare-release.outputs.codegen_version }} + title: prepare release ${{ steps.prepare-release.outputs.codegen_version }} + branch: prepare-release-${{ steps.prepare-release.outputs.codegen_version }} + base: 3.0.0 + body: | + Prepare Swagger Codegen ${{ steps.prepare-release.outputs.codegen_version }}. + + release_generators: ${{ inputs.release_generators }} + swagger-codegen-generators version: ${{ steps.prepare-release.outputs.generators_version }} + next codegen snapshot: ${{ steps.prepare-release.outputs.next_codegen_snapshot_version }} diff --git a/CI/release/common.sh b/CI/release/common.sh new file mode 100644 index 00000000000..5603b8ef9c9 --- /dev/null +++ b/CI/release/common.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +set -euo pipefail + +RELEASED_MAVEN_BASE="${RELEASED_MAVEN_BASE:-https://repo1.maven.org/maven2}" +SNAPSHOT_MAVEN_BASE="${SNAPSHOT_MAVEN_BASE:-https://central.sonatype.com/repository/maven-snapshots}" +CODEGEN_GROUP_PATH="io/swagger/codegen/v3" +GENERATORS_ARTIFACT="swagger-codegen-generators" + +fail() { + echo "::error::$*" + exit 1 +} + +require_release_version() { + local name="$1" + local version="$2" + + [[ -n "${version}" ]] || fail "${name} is required" + [[ ! "${version}" =~ SNAPSHOT$ ]] || fail "${name} must be a release version, got ${version}" + [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "${name} must match X.Y.Z, got ${version}" +} + +require_release_or_snapshot_version() { + local name="$1" + local version="$2" + + [[ -n "${version}" ]] || fail "${name} is required" + [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-SNAPSHOT)?$ ]] || fail "${name} must match X.Y.Z or X.Y.Z-SNAPSHOT, got ${version}" +} + +require_snapshot_version() { + local name="$1" + local version="$2" + + [[ -n "${version}" ]] || fail "${name} is required" + [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-SNAPSHOT$ ]] || fail "${name} must match X.Y.Z-SNAPSHOT, got ${version}" +} + +release_from_snapshot_version() { + local version="$1" + + [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-SNAPSHOT$ ]] || fail "Cannot derive release version from non-SNAPSHOT version ${version}" + printf '%s\n' "${version%-SNAPSHOT}" +} + +next_snapshot_from_release_version() { + local version="$1" + local major minor patch + + require_release_version "version" "${version}" + IFS=. read -r major minor patch <<< "${version}" + printf '%s.%s.%s-SNAPSHOT\n' "${major}" "${minor}" "$((patch + 1))" +} + +## Thin Maven helpers for reading project-level values from pom.xml. +maven_project_version() { + mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive org.codehaus.mojo:exec-maven-plugin:1.3.1:exec +} + +## Shared curl wrapper for metadata fetch with retries and hard timeouts. +curl_metadata() { + local url="$1" + curl --fail --silent --show-error --location --retry 5 --retry-delay 2 --connect-timeout 20 --max-time 90 "${url}" +} + +## Parse entries from Maven metadata.xml. +versions_from_metadata() { + awk -F'[<>]' '//{print $3}' +} + +## Resolve the newest version matching a regex pattern from metadata. +latest_matching_version() { + local metadata_url="$1" + local pattern="$2" + local version + + version="$(curl_metadata "${metadata_url}" | versions_from_metadata | grep -E "${pattern}" | sort -V | tail -n 1 || true)" + [[ -n "${version}" ]] || fail "No version matching '${pattern}' found in ${metadata_url}" + printf '%s\n' "${version}" +} + +latest_released_generators_version() { + latest_matching_version "${RELEASED_MAVEN_BASE}/${CODEGEN_GROUP_PATH}/${GENERATORS_ARTIFACT}/maven-metadata.xml" '^1\.[0-9]+\.[0-9]+$' +} + +latest_snapshot_generators_version() { + latest_matching_version "${SNAPSHOT_MAVEN_BASE}/${CODEGEN_GROUP_PATH}/${GENERATORS_ARTIFACT}/maven-metadata.xml" '^1\.[0-9]+\.[0-9]+-SNAPSHOT$' +} + +## Build canonical artifact URLs and probe existence without downloading payloads. +release_artifact_url() { + local artifact="$1" + local version="$2" + + printf '%s/%s/%s/%s/%s-%s.pom\n' "${RELEASED_MAVEN_BASE}" "${CODEGEN_GROUP_PATH}" "${artifact}" "${version}" "${artifact}" "${version}" +} + +release_artifact_exists() { + local artifact="$1" + local version="$2" + local url + + url="$(release_artifact_url "${artifact}" "${version}")" + curl --fail --silent --show-error --head --location --retry 3 --connect-timeout 20 --max-time 60 "${url}" >/dev/null 2>&1 +} + +## SNAPSHOT coordinates resolve via maven-metadata.xml, not fixed file names. +snapshot_metadata_url() { + local artifact="$1" + local version="$2" + + printf '%s/%s/%s/%s/maven-metadata.xml\n' "${SNAPSHOT_MAVEN_BASE}" "${CODEGEN_GROUP_PATH}" "${artifact}" "${version}" +} + +assert_snapshot_metadata_exists() { + local artifact="$1" + local version="$2" + local metadata_url + + metadata_url="$(snapshot_metadata_url "${artifact}" "${version}")" + if ! curl_metadata "${metadata_url}" >/dev/null; then + fail "Required SNAPSHOT ${CODEGEN_GROUP_PATH}:${artifact}:${version} cannot be resolved from ${metadata_url}. Sonatype snapshots can expire. Recovery: publish that exact snapshot version, then rerun this workflow." + fi +} diff --git a/CI/release/prepare-codegen-release.sh b/CI/release/prepare-codegen-release.sh new file mode 100644 index 00000000000..b0578a69baf --- /dev/null +++ b/CI/release/prepare-codegen-release.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +codegen_version="${CODEGEN_VERSION:-}" +next_codegen_snapshot_version="${NEXT_CODEGEN_SNAPSHOT_VERSION:-}" +release_generators="${RELEASE_GENERATORS:-false}" +generators_version="${GENERATORS_VERSION:-}" +previous_generators_version="${PREVIOUS_GENERATORS_VERSION:-}" +build_generators_version="" + +# Prepare flow must start from a SNAPSHOT on branch 3.0.0. +current_version="$(maven_project_version)" +[[ "${current_version}" =~ SNAPSHOT$ ]] || fail "Prepare release must start from a SNAPSHOT codegen version, got ${current_version}" + +## Resolve target release + next snapshot versions when not provided explicitly. +if [[ -z "${codegen_version}" ]]; then + codegen_version="$(release_from_snapshot_version "${current_version}")" +fi +require_release_version "CODEGEN_VERSION" "${codegen_version}" + +if [[ -z "${next_codegen_snapshot_version}" ]]; then + next_codegen_snapshot_version="$(next_snapshot_from_release_version "${codegen_version}")" +fi +require_snapshot_version "NEXT_CODEGEN_SNAPSHOT_VERSION" "${next_codegen_snapshot_version}" + +if [[ "${release_generators}" == "true" ]]; then + # Releasing generators: release version is explicit, build can bootstrap from previous snapshot/release. + require_release_version "GENERATORS_VERSION" "${generators_version}" + if [[ -n "${previous_generators_version}" ]]; then + require_release_or_snapshot_version "PREVIOUS_GENERATORS_VERSION" "${previous_generators_version}" + build_generators_version="${previous_generators_version}" + else + build_generators_version="$(latest_snapshot_generators_version)" + fi +else + # Not releasing generators: pin codegen to an already released generators artifact. + if [[ -z "${generators_version}" ]]; then + generators_version="$(latest_released_generators_version)" + fi + require_release_version "resolved generators version" "${generators_version}" + build_generators_version="${generators_version}" +fi + +## Validate the exact generators coordinate used for the candidate build. +if [[ "${build_generators_version}" =~ SNAPSHOT$ ]]; then + assert_snapshot_metadata_exists "${GENERATORS_ARTIFACT}" "${build_generators_version}" +else + release_artifact_exists "${GENERATORS_ARTIFACT}" "${build_generators_version}" || fail "Generator release ${build_generators_version} does not exist in Maven Central" +fi + +echo "Preparing codegen ${codegen_version} from ${current_version}" +echo "Using swagger-codegen-generators ${generators_version}" +echo "Building release candidate with swagger-codegen-generators ${build_generators_version}" + +## Expose resolved values to later workflow steps and PR metadata. +if [[ -n "${GITHUB_ENV:-}" ]]; then + echo "GENERATORS_VERSION=${generators_version}" >> "${GITHUB_ENV}" + echo "BUILD_GENERATORS_VERSION=${build_generators_version}" >> "${GITHUB_ENV}" + echo "CODEGEN_VERSION=${codegen_version}" >> "${GITHUB_ENV}" + echo "NEXT_CODEGEN_SNAPSHOT_VERSION=${next_codegen_snapshot_version}" >> "${GITHUB_ENV}" +fi + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "generators_version=${generators_version}" >> "${GITHUB_OUTPUT}" + echo "build_generators_version=${build_generators_version}" >> "${GITHUB_OUTPUT}" + echo "codegen_version=${codegen_version}" >> "${GITHUB_OUTPUT}" + echo "next_codegen_snapshot_version=${next_codegen_snapshot_version}" >> "${GITHUB_OUTPUT}" +fi + +## Move project from snapshot to release version before file-level content updates. +mvn -B versions:set -DnewVersion="${codegen_version}" +mvn -B versions:commit + +# Generate a minimal release-notes draft aligned with current GitHub release style. +mkdir -p docs/release-notes +previous_tag="$(git tag --merged HEAD --list 'v3.*' | sort -V | tail -n 1 || true)" +release_notes_file="docs/release-notes/v${codegen_version}.md" +{ + echo "# Swagger Codegen v${codegen_version}" + echo + echo "## What's Changed" + echo + if [[ -n "${previous_tag}" ]]; then + git log --first-parent --pretty=format:'* %s' "${previous_tag}..HEAD" + echo + echo + echo "Full Changelog: ${previous_tag}...v${codegen_version}" + else + git log --first-parent --pretty=format:'* %s' HEAD + echo + echo + echo "Full Changelog: initial...v${codegen_version}" + echo + fi +} > "${release_notes_file}" + +update_codegen_release_files_script="CI/release/update-codegen-release-files.py" +[[ -f "${update_codegen_release_files_script}" ]] || fail "Missing ${update_codegen_release_files_script}" + +## Keep docs/poms/openapi in sync with the release state. +python3 "${update_codegen_release_files_script}" prepare \ + "${codegen_version}" \ + "${next_codegen_snapshot_version}" \ + "${generators_version}" + +echo "Prepared release file updates for ${codegen_version}" diff --git a/CI/release/update-codegen-release-files.py b/CI/release/update-codegen-release-files.py new file mode 100644 index 00000000000..816a5811a54 --- /dev/null +++ b/CI/release/update-codegen-release-files.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import re +import sys + +COMPATIBILITY_DOCS = ["README.md", "docs/compatibility.md"] +RELEASE_DOCS = ["README.md", "docs/prerequisites.md", "docs/versioning.md"] + +# Regex patterns intentionally target release tables/examples used in public docs. +SNAPSHOT_ROW_PATTERN = ( + r"\| [0-9]+\.[0-9]+\.[0-9]+-SNAPSHOT \(current 3\.0\.0, upcoming minor release\).*?\| Minor release\s*\|" +) +RELEASE_ROW_PATTERN = ( + r"\| \[[0-9]+\.[0-9]+\.[0-9]+\]\(https://github\.com/swagger-api/swagger-codegen/releases/tag/v[0-9]+\.[0-9]+\.[0-9]+\) " + r"(?:\(\*\*current stable\*\*\)\s*)?.*?\| \[tag v[0-9]+\.[0-9]+\.[0-9]+\]\(https://github\.com/swagger-api/swagger-codegen/tree/v[0-9]+\.[0-9]+\.[0-9]+\)\s*\|" +) +RELEASE_JAR_PATTERN = ( + r"io/swagger/codegen/v3/swagger-codegen-cli/[0-9]+\.[0-9]+\.[0-9]+/swagger-codegen-cli-[0-9]+\.[0-9]+\.[0-9]+\.jar" +) +RELEASE_VERSION_PATTERN = r"3\.0\.[0-9]+" + + +def replace_text(path: str, replacements: list[tuple[str, str]]) -> None: + # Generic file replacement helper; no-op if target file is absent. + file_path = Path(path) + if not file_path.exists(): + return + text = file_path.read_text() + original = text + for pattern, value in replacements: + text = re.sub(pattern, value, text, flags=re.MULTILINE) + if text != original: + file_path.write_text(text) + + +def replace_text_in_docs( + docs: list[str], replacements: list[tuple[str, str]], *, count: int = 0, flags: int = re.MULTILINE, require_match: bool = False +) -> None: + # Apply replacement sets across a known docs list. + for doc in docs: + file_path = Path(doc) + if not file_path.exists(): + continue + text = file_path.read_text() + original = text + matched_any = False + for pattern, value in replacements: + text, replaced = re.subn(pattern, value, text, count=count, flags=flags) + matched_any = matched_any or replaced > 0 + if require_match and not matched_any: + raise RuntimeError(f"No replacements matched in {doc}") + if text != original: + file_path.write_text(text) + + +def update_generators_poms(generators_version: str) -> None: + # Keep generators dependency aligned in both root pom variants. + replacements = [ + ( + r"[^<]+", + f"{generators_version}", + ), + ] + replace_text("pom.xml", replacements) + replace_text("pom.docker.xml", replacements) + + +def update_openapi_version(version: str) -> None: + # Reflect current codegen version in online generator OpenAPI metadata. + replace_text( + "modules/swagger-generator/src/main/resources/openapi.yaml", + [(r"^ version: .*$", f" version: {version}")], + ) + + +def update_snapshot_rows(next_snapshot: str) -> None: + # Update "current upcoming snapshot" row in compatibility docs. + snapshot_row = ( + f"| {next_snapshot} (current 3.0.0, upcoming minor release) " + "[SNAPSHOT](https://central.sonatype.com/service/rest/repository/browse/maven-snapshots/" + f"io/swagger/codegen/v3/swagger-codegen-cli/{next_snapshot}/) | TBD | 1.0, 1.1, 1.2, 2.0, 3.0 | Minor release |" + ) + replace_text_in_docs(COMPATIBILITY_DOCS, [(SNAPSHOT_ROW_PATTERN, snapshot_row)], require_match=True) + + +def update_release_rows(codegen_version: str) -> None: + # Mark latest stable release row at the top of compatibility tables. + release_row = ( + f"| [{codegen_version}](https://github.com/swagger-api/swagger-codegen/releases/tag/v{codegen_version}) " + "(**current stable**) | TBD | 1.0, 1.1, 1.2, 2.0, 3.0 | " + f"[tag v{codegen_version}](https://github.com/swagger-api/swagger-codegen/tree/v{codegen_version}) |" + ) + replace_text_in_docs(COMPATIBILITY_DOCS, [(RELEASE_ROW_PATTERN, release_row)], count=1) + + +def update_release_docs(codegen_version: str) -> None: + # Point release examples to concrete released CLI coordinates. + replace_text_in_docs( + RELEASE_DOCS, + [ + ( + RELEASE_JAR_PATTERN, + f"io/swagger/codegen/v3/swagger-codegen-cli/{codegen_version}/swagger-codegen-cli-{codegen_version}.jar", + ), + (RELEASE_VERSION_PATTERN, f"{codegen_version}"), + ], + ) + + +def main() -> int: + if len(sys.argv) < 2: + print("usage: update-codegen-release-files.py prepare ", file=sys.stderr) + return 2 + + mode = sys.argv[1] + if mode == "prepare": + # Prepare mode: move docs/content to released codegen version state. + if len(sys.argv) != 5: + print("usage: ... prepare ", file=sys.stderr) + return 2 + codegen_version, next_snapshot, generators_version = sys.argv[2:5] + update_generators_poms(generators_version) + update_openapi_version(codegen_version) + update_snapshot_rows(next_snapshot) + update_release_rows(codegen_version) + update_release_docs(codegen_version) + return 0 + + print(f"unknown mode: {mode}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main())