Release #150
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version to release, without v prefix" | |
| required: true | |
| default: "1.0.1" | |
| release_tag: | |
| description: "Git tag to release from" | |
| required: true | |
| default: "v1.0.1" | |
| dry_run: | |
| description: "Build/package only; do not publish" | |
| required: true | |
| default: "true" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_crates: | |
| description: "Publish Rust crate to crates.io" | |
| required: true | |
| default: "true" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_npm: | |
| description: "Publish Node packages to npm" | |
| required: true | |
| default: "true" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_pypi: | |
| description: "Publish Python artifacts to PyPI" | |
| required: true | |
| default: "true" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_pub: | |
| description: "Publish metadata-only Dart reservation package" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_jsr: | |
| description: "Publish metadata-only TypeScript reservation package" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_hex: | |
| description: "Publish metadata-only Elixir reservation package" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_rubygems: | |
| description: "Publish metadata-only Ruby reservation package" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_nuget: | |
| description: "Publish metadata-only .NET reservation package" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_maven: | |
| description: "Publish metadata-only JVM reservation package" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_cocoapods: | |
| description: "Publish metadata-only Swift reservation package" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_hackage: | |
| description: "Publish metadata-only Haskell reservation package" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_packagist: | |
| description: "Publish metadata-only PHP reservation package" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| publish_ghcr: | |
| description: "Publish metadata container image to GHCR" | |
| required: true | |
| default: "false" | |
| type: choice | |
| options: ["true", "false"] | |
| baseline_ref: | |
| description: "Optional previous release ref for cargo-semver-checks" | |
| required: false | |
| default: "v1.0.0" | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| CARGO_INCREMENTAL: "0" | |
| SQLX_OFFLINE: "true" | |
| jobs: | |
| preflight: | |
| name: Preflight | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Confirm release ref and versions | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ inputs.version }}" | |
| TAG="${{ inputs.release_tag }}" | |
| if [[ "$TAG" != "v$VERSION" ]]; then | |
| echo "Release tag must be v$VERSION; got $TAG" >&2 | |
| exit 1 | |
| fi | |
| grep -q '^version = "'$VERSION'"$' Cargo.toml | |
| grep -q '"version": "'$VERSION'"' bindings/node/package.json | |
| grep -q '^version = "'$VERSION'"$' bindings/python/pyproject.toml | |
| grep -q '^version = "'$VERSION'"$' bindings/node/Cargo.toml | |
| grep -q '^version = "'$VERSION'"$' bindings/python/Cargo.toml | |
| # Reservation packages intentionally stay at 0.0.1. Selecting one for a | |
| # future publish requires an explicit version bump and therefore fails | |
| # here instead of presenting metadata as a stable 1.x API. | |
| if [[ "${{ inputs.publish_pub }}" == "true" ]]; then grep -q '^version: '$VERSION'$' bindings/dart/pubspec.yaml; fi | |
| if [[ "${{ inputs.publish_rubygems }}" == "true" ]]; then grep -q 'spec.version = "'$VERSION'"' bindings/ruby/forgelib.gemspec; fi | |
| if [[ "${{ inputs.publish_nuget }}" == "true" ]]; then grep -q '<Version>'$VERSION'</Version>' bindings/dotnet/ForgeLib.csproj; fi | |
| if [[ "${{ inputs.publish_maven }}" == "true" ]]; then grep -q '<version>'$VERSION'</version>' bindings/java/pom.xml; fi | |
| if [[ "${{ inputs.publish_packagist }}" == "true" ]]; then grep -q '"name": "isala404/forgelib"' composer.json; fi | |
| if [[ "${{ inputs.publish_cocoapods }}" == "true" ]]; then grep -q 'spec.version = "'$VERSION'"' bindings/swift/ForgeLib.podspec; fi | |
| if [[ "${{ inputs.publish_hex }}" == "true" ]]; then grep -q 'version: "'$VERSION'"' bindings/elixir/mix.exs; fi | |
| if [[ "${{ inputs.publish_hackage }}" == "true" ]]; then grep -q '^version: '$VERSION'$' bindings/haskell/forgelib.cabal; fi | |
| if [[ "${{ inputs.publish_jsr }}" == "true" ]]; then grep -q '"version": "'$VERSION'"' bindings/jsr/jsr.json; fi | |
| if [[ "${{ inputs.dry_run }}" != "true" && "$GITHUB_REF" != "refs/tags/$TAG" ]]; then | |
| echo "Real releases must be dispatched from $TAG; current ref is $GITHUB_REF" >&2 | |
| exit 1 | |
| fi | |
| - name: Confirm crates.io target is publishable | |
| if: ${{ inputs.publish_crates == 'true' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ inputs.version }}" | |
| curl --fail --silent --location \ | |
| --user-agent "forgelib-release-check/${VERSION}" \ | |
| https://index.crates.io/fo/rg/forgelib | VERSION="$VERSION" python3 -c ' | |
| import json | |
| import os | |
| import sys | |
| version = os.environ["VERSION"] | |
| for line in sys.stdin: | |
| if not line.strip(): | |
| continue | |
| item = json.loads(line) | |
| if item.get("vers") != version: | |
| continue | |
| if item.get("yanked"): | |
| print( | |
| f"crates.io forgelib@{version} exists but is yanked; " | |
| "crates.io versions are immutable, so this release cannot publish the Rust crate.", | |
| file=sys.stderr, | |
| ) | |
| sys.exit(1) | |
| print(f"::notice::crates.io forgelib@{version} already exists; publish will be skipped.") | |
| sys.exit(0) | |
| print(f"::notice::crates.io forgelib@{version} is available.") | |
| ' | |
| - name: Confirm publish credentials | |
| if: ${{ inputs.dry_run == 'false' }} | |
| shell: bash | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | |
| PUB_CREDENTIALS: ${{ secrets.PUB_CREDENTIALS }} | |
| JSR_TOKEN: ${{ secrets.JSR_TOKEN }} | |
| HEX_API_KEY: ${{ secrets.HEX_API_KEY }} | |
| RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }} | |
| NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} | |
| MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | |
| MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} | |
| MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} | |
| MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} | |
| COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} | |
| HACKAGE_USERNAME: ${{ secrets.HACKAGE_USERNAME }} | |
| HACKAGE_PASSWORD: ${{ secrets.HACKAGE_PASSWORD }} | |
| PACKAGIST_USERNAME: ${{ secrets.PACKAGIST_USERNAME }} | |
| PACKAGIST_API_TOKEN: ${{ secrets.PACKAGIST_API_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ inputs.version }}" | |
| missing=() | |
| require_secret() { | |
| local name="$1" | |
| if [[ -z "${!name:-}" ]]; then | |
| missing+=("$name") | |
| fi | |
| } | |
| version_exists() { | |
| local registry="$1" | |
| local package="${2:-}" | |
| REGISTRY="$registry" PACKAGE="$package" VERSION="$VERSION" python3 - <<'PY' | |
| import json | |
| import os | |
| import sys | |
| import urllib.error | |
| import urllib.parse | |
| import urllib.request | |
| registry = os.environ["REGISTRY"] | |
| package = os.environ.get("PACKAGE", "") | |
| version = os.environ["VERSION"] | |
| headers = {"User-Agent": f"forgelib-release-check/{version}"} | |
| def fetch_json(url): | |
| request = urllib.request.Request(url, headers=headers) | |
| with urllib.request.urlopen(request, timeout=15) as response: | |
| return json.load(response) | |
| def url_exists(url): | |
| try: | |
| request = urllib.request.Request(url, headers=headers, method="HEAD") | |
| with urllib.request.urlopen(request, timeout=15): | |
| return True | |
| except urllib.error.HTTPError as error: | |
| if error.code == 405: | |
| request = urllib.request.Request(url, headers=headers) | |
| with urllib.request.urlopen(request, timeout=15): | |
| return True | |
| return False | |
| except Exception: | |
| return False | |
| try: | |
| if registry == "crates": | |
| request = urllib.request.Request("https://index.crates.io/fo/rg/forgelib", headers=headers) | |
| with urllib.request.urlopen(request, timeout=15) as response: | |
| for line in response: | |
| if not line.strip(): | |
| continue | |
| item = json.loads(line) | |
| if item.get("vers") == version and not item.get("yanked"): | |
| sys.exit(0) | |
| sys.exit(1) | |
| if registry == "npm": | |
| quoted = urllib.parse.quote(package, safe="") | |
| data = fetch_json(f"https://registry.npmjs.org/{quoted}") | |
| sys.exit(0 if version in data.get("versions", {}) else 1) | |
| if registry == "pypi": | |
| data = fetch_json("https://pypi.org/pypi/forgelib/json") | |
| sys.exit(0 if version in data.get("releases", {}) else 1) | |
| if registry == "pub": | |
| data = fetch_json("https://pub.dev/api/packages/forgelib") | |
| versions = {item.get("version") for item in data.get("versions", [])} | |
| sys.exit(0 if version in versions else 1) | |
| if registry == "jsr": | |
| data = fetch_json("https://jsr.io/@isala404/forgelib/meta.json") | |
| sys.exit(0 if version in data.get("versions", {}) else 1) | |
| if registry == "hex": | |
| data = fetch_json("https://hex.pm/api/packages/forgelib") | |
| versions = {item.get("version") for item in data.get("releases", [])} | |
| sys.exit(0 if version in versions else 1) | |
| if registry == "rubygems": | |
| data = fetch_json("https://rubygems.org/api/v1/versions/forgelib.json") | |
| versions = {item.get("number") for item in data} | |
| sys.exit(0 if version in versions else 1) | |
| if registry == "nuget": | |
| data = fetch_json("https://api.nuget.org/v3-flatcontainer/forgelib/index.json") | |
| sys.exit(0 if version in data.get("versions", []) else 1) | |
| if registry == "maven": | |
| url = f"https://repo1.maven.org/maven2/io/github/isala404/forgelib/{version}/forgelib-{version}.pom" | |
| sys.exit(0 if url_exists(url) else 1) | |
| if registry == "cocoapods": | |
| url = f"https://trunk.cocoapods.org/api/v1/pods/ForgeLib/specs/{version}" | |
| sys.exit(0 if url_exists(url) else 1) | |
| if registry == "hackage": | |
| url = f"https://hackage.haskell.org/package/forgelib-{version}/forgelib.cabal" | |
| sys.exit(0 if url_exists(url) else 1) | |
| if registry == "packagist": | |
| data = fetch_json("https://repo.packagist.org/p2/isala404/forgelib.json") | |
| packages = data.get("packages", {}).get("isala404/forgelib", []) | |
| versions = {item.get("version") for item in packages} | |
| sys.exit(0 if version in versions else 1) | |
| except Exception: | |
| sys.exit(1) | |
| sys.exit(1) | |
| PY | |
| } | |
| require_when_missing() { | |
| local label="$1" | |
| local registry="$2" | |
| local package="$3" | |
| shift 3 | |
| if version_exists "$registry" "$package"; then | |
| echo "::notice::$label@$VERSION already exists; not requiring publish credentials." | |
| return | |
| fi | |
| echo "::notice::$label@$VERSION is not published yet; checking required publish credentials." | |
| for name in "$@"; do | |
| require_secret "$name" | |
| done | |
| } | |
| if [[ "${{ inputs.publish_crates }}" == "true" ]]; then | |
| require_when_missing "crates.io forgelib" crates "" CARGO_REGISTRY_TOKEN | |
| fi | |
| if [[ "${{ inputs.publish_npm }}" == "true" ]]; then | |
| if ! version_exists npm forgelib; then | |
| require_secret NPM_TOKEN | |
| else | |
| echo "::notice::npm forgelib@$VERSION already exists; not requiring NPM_TOKEN." | |
| fi | |
| fi | |
| if [[ "${{ inputs.publish_pypi }}" == "true" ]]; then | |
| if version_exists pypi ""; then | |
| echo "::notice::PyPI forgelib@$VERSION already exists; not requiring PyPI credentials." | |
| elif [[ -z "${PYPI_API_TOKEN:-}" ]]; then | |
| echo "::notice::PYPI_API_TOKEN is not set; PyPI will use trusted publishing and requires a configured PyPI publisher for this workflow." | |
| fi | |
| fi | |
| if [[ "${{ inputs.publish_pub }}" == "true" ]]; then | |
| require_when_missing "pub.dev forgelib" pub "" PUB_CREDENTIALS | |
| fi | |
| if [[ "${{ inputs.publish_jsr }}" == "true" ]]; then | |
| require_when_missing "JSR @isala404/forgelib" jsr "" JSR_TOKEN | |
| fi | |
| if [[ "${{ inputs.publish_hex }}" == "true" ]]; then | |
| require_when_missing "Hex forgelib" hex "" HEX_API_KEY | |
| fi | |
| if [[ "${{ inputs.publish_rubygems }}" == "true" ]]; then | |
| require_when_missing "RubyGems forgelib" rubygems "" RUBYGEMS_API_KEY | |
| fi | |
| if [[ "${{ inputs.publish_nuget }}" == "true" ]]; then | |
| require_when_missing "NuGet ForgeLib" nuget "" NUGET_API_KEY | |
| fi | |
| if [[ "${{ inputs.publish_maven }}" == "true" ]]; then | |
| require_when_missing "Maven Central io.github.isala404:forgelib" maven "" MAVEN_CENTRAL_USERNAME MAVEN_CENTRAL_TOKEN MAVEN_GPG_PRIVATE_KEY MAVEN_GPG_PASSPHRASE | |
| fi | |
| if [[ "${{ inputs.publish_cocoapods }}" == "true" ]]; then | |
| require_when_missing "CocoaPods ForgeLib" cocoapods "" COCOAPODS_TRUNK_TOKEN | |
| fi | |
| if [[ "${{ inputs.publish_hackage }}" == "true" ]]; then | |
| require_when_missing "Hackage forgelib" hackage "" HACKAGE_USERNAME HACKAGE_PASSWORD | |
| fi | |
| if [[ "${{ inputs.publish_packagist }}" == "true" ]]; then | |
| require_when_missing "Packagist isala404/forgelib" packagist "" PACKAGIST_USERNAME PACKAGIST_API_TOKEN | |
| fi | |
| if (( ${#missing[@]} > 0 )); then | |
| printf 'Missing secrets for real release:\n' >&2 | |
| printf ' - %s\n' "${missing[@]}" >&2 | |
| exit 1 | |
| fi | |
| quality: | |
| name: Quality | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| needs: preflight | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: release-quality | |
| workspaces: | | |
| . -> target | |
| bindings/node -> target | |
| bindings/python -> target | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - run: cargo fmt --all --check | |
| - run: cargo fmt --manifest-path bindings/node/Cargo.toml -- --check | |
| - run: cargo fmt --manifest-path bindings/python/Cargo.toml -- --check | |
| - run: cargo clippy --all-targets --all-features -- -D warnings | |
| - run: cargo clippy --manifest-path bindings/node/Cargo.toml --all-targets -- -D warnings | |
| - run: cargo clippy --manifest-path bindings/python/Cargo.toml --all-targets -- -D warnings | |
| - run: cargo build --all-features | |
| - run: cargo test | |
| - run: cargo run --manifest-path tools/codegen/Cargo.toml -- --check | |
| - run: python3 tools/api-contract/check.py | |
| - run: python3 tools/skill-check/check.py | |
| guardrails: | |
| name: Guardrails and package | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: preflight | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: release-guardrails | |
| - uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cargo-deny@0.19.5,cargo-audit@0.22.1 | |
| - run: cargo deny check | |
| - run: cargo audit --deny warnings | |
| - run: cargo audit --file bindings/node/Cargo.lock --deny warnings | |
| - run: cargo audit --file bindings/python/Cargo.lock --deny warnings | |
| - run: cargo package --locked | |
| semver: | |
| name: Rust patch compatibility | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: preflight | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: release-semver | |
| - uses: taiki-e/install-action@v2 | |
| if: ${{ inputs.baseline_ref != '' }} | |
| with: | |
| tool: cargo-semver-checks@0.48.0 | |
| - name: Optional Rust public API compatibility check | |
| if: ${{ inputs.baseline_ref != '' }} | |
| run: | | |
| cargo semver-checks check-release \ | |
| --release-type patch \ | |
| --baseline-rev "${{ inputs.baseline_ref }}" | |
| postgres-tests: | |
| name: Postgres Integration | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| needs: [quality, guardrails, semver] | |
| services: | |
| postgres: | |
| image: postgres:18 | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: forge | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| env: | |
| TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/forge | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: release-quality | |
| workspaces: | | |
| . -> target | |
| bindings/node -> target | |
| bindings/python -> target | |
| save-if: "false" | |
| - run: cargo test --features pg-tests -- --test-threads=4 | |
| conformance: | |
| name: Cross-Language Conformance | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 40 | |
| needs: [quality, guardrails, semver] | |
| services: | |
| postgres: | |
| image: postgres:18 | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: forge | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| env: | |
| TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/forge | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: release-quality | |
| workspaces: | | |
| . -> target | |
| bindings/node -> target | |
| bindings/python -> target | |
| save-if: "false" | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: astral-sh/setup-uv@v5 | |
| - run: bash tools/conformance/run-all.sh | |
| crates: | |
| name: crates.io | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_crates == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - run: cargo publish --dry-run | |
| - name: Check crate version | |
| id: crate_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if curl --fail --silent --location \ | |
| --user-agent "forgelib-release-check/${{ inputs.version }}" \ | |
| https://index.crates.io/fo/rg/forgelib | python3 -c ' | |
| import json | |
| import sys | |
| version = "${{ inputs.version }}" | |
| for line in sys.stdin: | |
| if not line.strip(): | |
| continue | |
| item = json.loads(line) | |
| if item.get("vers") != version: | |
| continue | |
| if item.get("yanked"): | |
| print( | |
| f"crates.io forgelib@{version} exists but is yanked; " | |
| "this version cannot be republished.", | |
| file=sys.stderr, | |
| ) | |
| sys.exit(2) | |
| else: | |
| sys.exit(0) | |
| sys.exit(1) | |
| '; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish crate | |
| if: ${{ inputs.dry_run == 'false' && steps.crate_exists.outputs.exists != 'true' }} | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| run: cargo publish | |
| node-native: | |
| name: Node Native (${{ matrix.platform }}) | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 35 | |
| needs: [quality, guardrails, semver] | |
| if: ${{ inputs.publish_npm == 'true' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| platform: linux-x64-gnu | |
| zig: false | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-musl | |
| platform: linux-x64-musl | |
| zig: true | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-gnu | |
| platform: linux-arm64-gnu | |
| zig: false | |
| - os: macos-14 | |
| target: aarch64-apple-darwin | |
| platform: darwin-arm64 | |
| zig: false | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| platform: win32-x64-msvc | |
| zig: false | |
| defaults: | |
| run: | |
| working-directory: bindings/node | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: release-node-${{ matrix.platform }} | |
| workspaces: bindings/node -> target | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - uses: goto-bus-stop/setup-zig@v2 | |
| if: ${{ matrix.zig }} | |
| with: | |
| version: 0.14.1 | |
| - uses: taiki-e/install-action@v2 | |
| if: ${{ matrix.zig }} | |
| with: | |
| tool: cargo-zigbuild@0.23.0 | |
| - run: npm ci --ignore-scripts --no-audit --no-fund | |
| - name: Build native addon | |
| shell: bash | |
| run: | | |
| if [[ "${{ matrix.zig }}" == "true" ]]; then | |
| npm run build -- --target ${{ matrix.target }} --cross-compile | |
| else | |
| npm run build -- --target ${{ matrix.target }} | |
| fi | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: forgelib-${{ matrix.platform }} | |
| path: bindings/node/forgelib.${{ matrix.platform }}.node | |
| if-no-files-found: error | |
| npm: | |
| name: npm | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [node-native, postgres-tests, conformance] | |
| if: ${{ inputs.publish_npm == 'true' }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| defaults: | |
| run: | |
| working-directory: bindings/node | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| registry-url: "https://registry.npmjs.org" | |
| - run: npm ci --ignore-scripts --no-audit --no-fund | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| pattern: forgelib-* | |
| path: bindings/node | |
| merge-multiple: true | |
| - name: Verify native artifacts | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| for artifact in \ | |
| forgelib.darwin-arm64.node \ | |
| forgelib.linux-arm64-gnu.node \ | |
| forgelib.linux-x64-gnu.node \ | |
| forgelib.linux-x64-musl.node \ | |
| forgelib.win32-x64-msvc.node | |
| do | |
| test -f "$artifact" | |
| done | |
| - name: Pack main package | |
| run: npm pack --dry-run --ignore-scripts | |
| - name: Check npm package versions | |
| id: npm_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if npm view forgelib@${{ inputs.version }} version >/dev/null 2>&1; then | |
| echo "main=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "main=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish main package | |
| if: ${{ inputs.dry_run == 'false' && steps.npm_exists.outputs.main != 'true' }} | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: npm publish --access public --provenance | |
| # One abi3 wheel per primary platform, so most users never fall back to | |
| # compiling the sdist (which needs a Rust toolchain). | |
| python-wheels: | |
| name: Python Wheel (${{ matrix.platform }}) | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 35 | |
| needs: [quality, guardrails, semver] | |
| if: ${{ inputs.publish_pypi == 'true' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| platform: linux-x64-gnu | |
| manylinux: auto | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-gnu | |
| platform: linux-arm64-gnu | |
| manylinux: "2_28" | |
| container: quay.io/pypa/manylinux_2_28_aarch64:latest | |
| - os: macos-14 | |
| target: aarch64-apple-darwin | |
| platform: darwin-arm64 | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| platform: win32-x64 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: PyO3/maturin-action@v1 | |
| with: | |
| command: build | |
| args: --manifest-path bindings/python/Cargo.toml --release --out dist | |
| target: ${{ matrix.target }} | |
| manylinux: ${{ matrix.manylinux || 'auto' }} | |
| container: ${{ matrix.container || '' }} | |
| sccache: true | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: wheel-${{ matrix.platform }} | |
| path: dist/*.whl | |
| if-no-files-found: error | |
| pypi: | |
| name: PyPI | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| needs: [python-wheels, postgres-tests, conformance] | |
| if: ${{ inputs.publish_pypi == 'true' }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - run: python -m pip install --upgrade pip twine "maturin>=1.5,<2" | |
| - name: Build sdist | |
| run: maturin sdist --manifest-path bindings/python/Cargo.toml --out dist | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| pattern: wheel-* | |
| path: dist | |
| merge-multiple: true | |
| - run: twine check dist/* | |
| - name: Check PyPI version | |
| id: pypi_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if python - <<'PY' | |
| import json | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| version = "${{ inputs.version }}" | |
| try: | |
| with urllib.request.urlopen("https://pypi.org/pypi/forgelib/json", timeout=15) as response: | |
| data = json.load(response) | |
| except urllib.error.HTTPError as error: | |
| if error.code == 404: | |
| sys.exit(1) | |
| raise | |
| sys.exit(0 if version in data.get("releases", {}) else 1) | |
| PY | |
| then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Select PyPI auth mode | |
| id: pypi_auth | |
| if: ${{ inputs.dry_run == 'false' && steps.pypi_exists.outputs.exists != 'true' }} | |
| shell: bash | |
| env: | |
| PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${PYPI_API_TOKEN:-}" ]]; then | |
| echo "mode=token" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "mode=trusted" >> "$GITHUB_OUTPUT" | |
| echo "::notice::PYPI_API_TOKEN is not set; using PyPI trusted publishing." | |
| fi | |
| - name: Publish Python artifacts with PyPI API token | |
| if: ${{ inputs.dry_run == 'false' && steps.pypi_exists.outputs.exists != 'true' && steps.pypi_auth.outputs.mode == 'token' }} | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: dist | |
| skip-existing: true | |
| password: ${{ secrets.PYPI_API_TOKEN }} | |
| - name: Publish Python artifacts with trusted publishing | |
| if: ${{ inputs.dry_run == 'false' && steps.pypi_exists.outputs.exists != 'true' && steps.pypi_auth.outputs.mode == 'trusted' }} | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: dist | |
| skip-existing: true | |
| dart: | |
| name: pub.dev | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_pub == 'true' }} | |
| defaults: | |
| run: | |
| working-directory: bindings/dart | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dart-lang/setup-dart@v1 | |
| - run: dart pub publish --dry-run | |
| - name: Check pub.dev version | |
| id: pub_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if python3 - <<'PY' | |
| import json | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| version = "${{ inputs.version }}" | |
| try: | |
| with urllib.request.urlopen("https://pub.dev/api/packages/forgelib", timeout=15) as response: | |
| data = json.load(response) | |
| except urllib.error.HTTPError as error: | |
| if error.code == 404: | |
| sys.exit(1) | |
| raise | |
| versions = {item.get("version") for item in data.get("versions", [])} | |
| sys.exit(0 if version in versions else 1) | |
| PY | |
| then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Configure pub credentials | |
| if: ${{ inputs.dry_run == 'false' && steps.pub_exists.outputs.exists != 'true' }} | |
| env: | |
| PUB_CREDENTIALS: ${{ secrets.PUB_CREDENTIALS }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$HOME/.config/dart" | |
| printf '%s' "$PUB_CREDENTIALS" > "$HOME/.config/dart/pub-credentials.json" | |
| - name: Publish Dart package | |
| if: ${{ inputs.dry_run == 'false' && steps.pub_exists.outputs.exists != 'true' }} | |
| run: dart pub publish --force | |
| jsr: | |
| name: JSR | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_jsr == 'true' }} | |
| defaults: | |
| run: | |
| working-directory: bindings/jsr | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - run: npx jsr publish --dry-run --allow-dirty | |
| - name: Check JSR version | |
| id: jsr_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if python3 - <<'PY' | |
| import json | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| version = "${{ inputs.version }}" | |
| try: | |
| with urllib.request.urlopen("https://jsr.io/@isala404/forgelib/meta.json", timeout=15) as response: | |
| data = json.load(response) | |
| except urllib.error.HTTPError as error: | |
| if error.code == 404: | |
| sys.exit(1) | |
| raise | |
| sys.exit(0 if version in data.get("versions", {}) else 1) | |
| PY | |
| then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish JSR package | |
| if: ${{ inputs.dry_run == 'false' && steps.jsr_exists.outputs.exists != 'true' }} | |
| env: | |
| JSR_TOKEN: ${{ secrets.JSR_TOKEN }} | |
| run: npx jsr publish --allow-dirty | |
| hex: | |
| name: Hex | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_hex == 'true' }} | |
| defaults: | |
| run: | |
| working-directory: bindings/elixir | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: erlef/setup-beam@v1 | |
| with: | |
| otp-version: "27" | |
| elixir-version: "1.17" | |
| - run: mix local.hex --force | |
| - run: mix hex.build && rm -f forgelib-${{ inputs.version }}.tar | |
| - name: Check Hex version | |
| id: hex_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if python3 - <<'PY' | |
| import json | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| version = "${{ inputs.version }}" | |
| try: | |
| with urllib.request.urlopen("https://hex.pm/api/packages/forgelib", timeout=15) as response: | |
| data = json.load(response) | |
| except urllib.error.HTTPError as error: | |
| if error.code == 404: | |
| sys.exit(1) | |
| raise | |
| versions = {item.get("version") for item in data.get("releases", [])} | |
| sys.exit(0 if version in versions else 1) | |
| PY | |
| then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish Hex package | |
| if: ${{ inputs.dry_run == 'false' && steps.hex_exists.outputs.exists != 'true' }} | |
| env: | |
| HEX_API_KEY: ${{ secrets.HEX_API_KEY }} | |
| run: mix hex.publish package --yes | |
| rubygems: | |
| name: RubyGems | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_rubygems == 'true' }} | |
| defaults: | |
| run: | |
| working-directory: bindings/ruby | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: "3.3" | |
| - run: gem build forgelib.gemspec | |
| - name: Check RubyGems version | |
| id: gem_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if gem list --remote --exact forgelib --all | grep -F "(${{ inputs.version }})" >/dev/null; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish Ruby gem | |
| if: ${{ inputs.dry_run == 'false' && steps.gem_exists.outputs.exists != 'true' }} | |
| env: | |
| RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$HOME/.gem" | |
| printf -- "---\n:rubygems_api_key: %s\n" "$RUBYGEMS_API_KEY" > "$HOME/.gem/credentials" | |
| chmod 0600 "$HOME/.gem/credentials" | |
| gem push forgelib-${{ inputs.version }}.gem | |
| nuget: | |
| name: NuGet | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_nuget == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - name: Pack NuGet package | |
| run: dotnet pack bindings/dotnet/ForgeLib.csproj --configuration Release --output bindings/dotnet/dist /p:PackageVersion=${{ inputs.version }} | |
| - name: Check NuGet version | |
| id: nuget_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if python3 - <<'PY' | |
| import json | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| version = "${{ inputs.version }}" | |
| try: | |
| with urllib.request.urlopen("https://api.nuget.org/v3-flatcontainer/forgelib/index.json", timeout=15) as response: | |
| data = json.load(response) | |
| except urllib.error.HTTPError as error: | |
| if error.code == 404: | |
| sys.exit(1) | |
| raise | |
| sys.exit(0 if version in data.get("versions", []) else 1) | |
| PY | |
| then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish NuGet package | |
| if: ${{ inputs.dry_run == 'false' && steps.nuget_exists.outputs.exists != 'true' }} | |
| run: dotnet nuget push "bindings/dotnet/dist/ForgeLib.${{ inputs.version }}.nupkg" --api-key "${{ secrets.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate | |
| maven: | |
| name: Maven Central | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_maven == 'true' }} | |
| defaults: | |
| run: | |
| working-directory: bindings/java | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: "17" | |
| server-id: central | |
| server-username: MAVEN_CENTRAL_USERNAME | |
| server-password: MAVEN_CENTRAL_TOKEN | |
| gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} | |
| gpg-passphrase: MAVEN_GPG_PASSPHRASE | |
| - run: mvn -B package | |
| - name: Check Maven Central version | |
| id: maven_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if curl --fail --silent https://repo1.maven.org/maven2/io/github/isala404/forgelib/${{ inputs.version }}/forgelib-${{ inputs.version }}.pom >/dev/null; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish Maven package | |
| if: ${{ inputs.dry_run == 'false' && steps.maven_exists.outputs.exists != 'true' }} | |
| env: | |
| MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | |
| MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} | |
| MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} | |
| run: mvn -B -P central deploy | |
| cocoapods: | |
| name: CocoaPods | |
| runs-on: macos-14 | |
| timeout-minutes: 20 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_cocoapods == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Prepare root podspec | |
| run: cp bindings/swift/ForgeLib.podspec ForgeLib.podspec | |
| - name: Lint podspec | |
| run: pod lib lint ForgeLib.podspec --allow-warnings --skip-tests | |
| - name: Check CocoaPods version | |
| id: pod_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if curl --fail --silent "https://trunk.cocoapods.org/api/v1/pods/ForgeLib/specs/${{ inputs.version }}" >/dev/null; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish podspec | |
| if: ${{ inputs.dry_run == 'false' && steps.pod_exists.outputs.exists != 'true' }} | |
| env: | |
| COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} | |
| run: pod trunk push ForgeLib.podspec --allow-warnings --skip-tests | |
| hackage: | |
| name: Hackage | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_hackage == 'true' }} | |
| defaults: | |
| run: | |
| working-directory: bindings/haskell | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: haskell-actions/setup@v2 | |
| with: | |
| ghc-version: "9.8" | |
| cabal-version: "3.10" | |
| - run: cabal check | |
| - run: cabal sdist | |
| - name: Check Hackage version | |
| id: hackage_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if curl --fail --silent "https://hackage.haskell.org/package/forgelib-${{ inputs.version }}/forgelib.cabal" >/dev/null; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish Hackage package | |
| if: ${{ inputs.dry_run == 'false' && steps.hackage_exists.outputs.exists != 'true' }} | |
| env: | |
| HACKAGE_USERNAME: ${{ secrets.HACKAGE_USERNAME }} | |
| HACKAGE_PASSWORD: ${{ secrets.HACKAGE_PASSWORD }} | |
| run: cabal upload --publish --username="$HACKAGE_USERNAME" --password="$HACKAGE_PASSWORD" "dist-newstyle/sdist/forgelib-${{ inputs.version }}.tar.gz" | |
| php: | |
| name: PHP / Packagist | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_packagist == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: "8.3" | |
| tools: composer:v2 | |
| - run: composer validate --strict composer.json | |
| - run: composer validate --strict bindings/php/composer.json | |
| - name: Check Packagist package | |
| id: packagist_exists | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' >> "$GITHUB_OUTPUT" | |
| import json | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| version = "${{ inputs.version }}" | |
| try: | |
| with urllib.request.urlopen("https://repo.packagist.org/p2/isala404/forgelib.json", timeout=15) as response: | |
| data = json.load(response) | |
| except urllib.error.HTTPError as error: | |
| if error.code == 404: | |
| print("package=false") | |
| print("version=false") | |
| sys.exit(0) | |
| raise | |
| packages = data.get("packages", {}).get("isala404/forgelib", []) | |
| versions = {item.get("version") for item in packages} | |
| print("package=true") | |
| print(f"version={'true' if version in versions else 'false'}") | |
| PY | |
| - name: Create Packagist package | |
| if: ${{ inputs.dry_run == 'false' && steps.packagist_exists.outputs.package != 'true' }} | |
| env: | |
| PACKAGIST_USERNAME: ${{ secrets.PACKAGIST_USERNAME }} | |
| PACKAGIST_API_TOKEN: ${{ secrets.PACKAGIST_API_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| curl --fail --request POST \ | |
| "https://packagist.org/api/create-package" \ | |
| --header "Authorization: Bearer ${PACKAGIST_USERNAME}:${PACKAGIST_API_TOKEN}" \ | |
| --header "Content-Type: application/json" \ | |
| --data '{"repository":"https://github.com/isala404/forge"}' | |
| - name: Request Packagist update | |
| if: ${{ inputs.dry_run == 'false' && steps.packagist_exists.outputs.package == 'true' && steps.packagist_exists.outputs.version != 'true' }} | |
| env: | |
| PACKAGIST_USERNAME: ${{ secrets.PACKAGIST_USERNAME }} | |
| PACKAGIST_API_TOKEN: ${{ secrets.PACKAGIST_API_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| curl --fail --request POST \ | |
| "https://packagist.org/api/update-package" \ | |
| --header "Authorization: Bearer ${PACKAGIST_USERNAME}:${PACKAGIST_API_TOKEN}" \ | |
| --header "Content-Type: application/json" \ | |
| --data '{"repository":"https://github.com/isala404/forge"}' | |
| r: | |
| name: R / CRAN check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [postgres-tests, conformance] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: r-lib/actions/setup-r@v2 | |
| - name: Build R package | |
| run: R CMD build bindings/r | |
| - name: Check R package | |
| run: R CMD check --no-manual forgelib_*.tar.gz | |
| go: | |
| name: Go module | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [postgres-tests, conformance] | |
| defaults: | |
| run: | |
| working-directory: bindings/go | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.22" | |
| - run: go test ./... | |
| swift: | |
| name: SwiftPM | |
| runs-on: macos-14 | |
| timeout-minutes: 15 | |
| needs: [postgres-tests, conformance] | |
| defaults: | |
| run: | |
| working-directory: bindings/swift | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - run: swift package dump-package | |
| ghcr: | |
| name: GHCR | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [postgres-tests, conformance] | |
| if: ${{ inputs.publish_ghcr == 'true' }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/login-action@v3 | |
| if: ${{ inputs.dry_run == 'false' }} | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - uses: docker/build-push-action@v6 | |
| with: | |
| context: bindings/container | |
| tags: ghcr.io/isala404/forgelib:${{ inputs.version }} | |
| push: ${{ inputs.dry_run == 'false' }} | |
| github-release: | |
| name: GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: | |
| - preflight | |
| - quality | |
| - guardrails | |
| - semver | |
| - postgres-tests | |
| - conformance | |
| - crates | |
| - node-native | |
| - npm | |
| - python-wheels | |
| - pypi | |
| - dart | |
| - jsr | |
| - hex | |
| - rubygems | |
| - nuget | |
| - maven | |
| - cocoapods | |
| - hackage | |
| - php | |
| - r | |
| - go | |
| - swift | |
| - ghcr | |
| if: ${{ always() && inputs.dry_run == 'false' && !cancelled() && !failure() }} | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Create immutable GitHub release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ inputs.version }} | |
| TAG: ${{ inputs.release_tag }} | |
| run: | | |
| set -euo pipefail | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "::notice::GitHub release $TAG already exists; leaving it unchanged." | |
| exit 0 | |
| fi | |
| NOTES="$RUNNER_TEMP/forgelib-release-notes.md" | |
| python3 - <<'PY' > "$NOTES" | |
| import os | |
| from pathlib import Path | |
| version = os.environ["VERSION"] | |
| changelog = Path("CHANGELOG.md").read_text() | |
| marker = f"## [{version}]" | |
| start = changelog.find(marker) | |
| if start < 0: | |
| raise SystemExit(f"CHANGELOG.md has no {marker} section") | |
| end = changelog.find("\n## [", start + len(marker)) | |
| print(changelog[start : end if end >= 0 else None].strip()) | |
| PY | |
| gh release create "$TAG" \ | |
| --verify-tag \ | |
| --title "Forge $VERSION" \ | |
| --notes-file "$NOTES" \ | |
| --latest |