Search by

ondewo / sip-client-php

ondewo

ONDEWO SIP (SIP Telephony) gRPC client for PHP, generated from the ondewo-sip-api protocol buffer definitions

Package info

github.com/ondewo/ondewo-sip-client-php

pkg:composer/ondewo/sip-client-php

Statistics

Installs: 41

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

5.5.2 2026-10-15 18:32 UTC

This package is auto-updated.

Last update: 2026-10-15 18:32:57 UTC


README

ONDEWO SIP Client PHP Library

This library is the PHP gRPC client for the ONDEWO SIP (SIP Telephony) server.

There is no hand-written transport layer in this repository. The entire client surface — messages, enums and one <Service>Client stub per gRPC service — is generated from the protocol buffer definitions of the ondewo-sip-api repository by the ONDEWO proto compiler, which is vendored here as a git submodule and pinned to a release tag. The generated stubs are committed — the repository root is the composer package, Packagist serves the tree of a git tag verbatim and composer has no build step, so what is committed here is exactly what a consumer receives.

The only hand-written PHP is the bearer-token authentication surface in auth/, which turns a token into the $opts array a generated stub is constructed with.

Requirements

  • PHP >= 8.1
  • The grpc PHP extension (ext-grpc) — every generated <Service>Client extends \Grpc\BaseStub
  • The bcmath PHP extension (ext-bcmath) — only to parse integers out of the JSON wire format; google/protobuf merely suggests it, so mergeFromJsonString() on a message with an int field fails without it
  • Composer 2.x
  • Docker — only to regenerate the stubs, never to use the client

Installing the extension:

# Debian / Ubuntu
sudo apt-get install -y php-grpc php-bcmath
# or, from source
sudo pecl install grpc

Installation

The client is published on Packagist as ondewo/sip-client-php and is installed like any other composer package:

composer require ondewo/sip-client-php

The client version tracks the ONDEWO SIP API in major and minor version, so pin the minor series that matches the server you talk to:

composer require ondewo/sip-client-php:^5.5

or, in composer.json:

{
  "require": {
    "ondewo/sip-client-php": "^5.5"
  }
}

Two things a consumer has to know:

  • ext-grpc is a hard requirement, not an optional extra — every generated <Service>Client extends \Grpc\BaseStub, so composer refuses to install the package without it. Install it first (see Requirements above); composer require ondewo/sip-client-php --ignore-platform-req=ext-grpc will resolve, but the client will fatal at runtime. ext-bcmath is not declared — google/protobuf only suggests it — but its pure-PHP JSON parser range-checks integers with bccomp(), so install it too if you read or write the JSON wire format.
  • There is no build step. Packagist serves the tree of a git tag verbatim and the generated stubs are committed, so the package a consumer downloads is byte-for-byte the tagged repository: after composer require, vendor/ondewo/sip-client-php/src/ already holds every stub and the autoloader is generated by composer itself. Nothing needs docker, protoc or the ondewo-sip-api submodule.

Everything is then reachable through composer's autoloader:

require __DIR__ . '/vendor/autoload.php';

use Ondewo\Sip\Auth\BearerTokenAuthenticator;   // hand-written, from auth/
use Ondewo\Sip\SipClient;                       // generated, from src/

To work on the client itself:

git clone --recurse-submodules git@github.com:ondewo/ondewo-sip-client-php.git
cd ondewo-sip-client-php
make setup_developer_environment_locally

make help lists every documented target; make makefile_chapters lists the Makefile's sections.

Repository structure

.
├── ondewo-sip-api          <----- submodule: the .proto definitions (ondewo/ = the services, google/ = imports)
├── ondewo-proto-compiler   <----- submodule: the compiler images, pinned to tags/5.15.5
├── auth                    <----- HAND-WRITTEN sources (bearer token authenticator, ClientConfig: TLS / mutual TLS)
├── src                     <----- GENERATED stubs, committed - compiler-owned, wiped on every generation run
│   ├── GPBMetadata         <----- descriptor bootstrap, one class per .proto
│   └── Ondewo              <----- messages, enums and the <Service>Client stubs
├── tests                   <----- PHPUnit suite (not part of the published classmap)
├── tools                   <----- dev-only composer project (PHPUnit + coverage gate), the pinned tool images, the rpc drift check
├── vendor                  <----- composer dependencies (gitignored)
├── composer.json           <----- the package manifest; MERGED with the compiler defaults on every run
├── phpunit.xml.dist        <----- test suite + coverage scope (auth/ only)
└── Makefile                <----- build, test and release automation

Three rules follow from that layout and matter more than anything else in this file:

  1. Never put hand-written PHP in src/. It is deleted and rewritten on every generation run. Hand-written code belongs in auth/ at the repository root — the compiler image detects that directory and adds it to the shipped autoloader's classmap itself.
  2. Never pin google/protobuf or grpc/grpc in composer.json to anything other than the versions the compiler image ships. The image resolves the merged manifest offline from a cache it pre-warmed at image-build time; a pin outside that cache fails the generation run.
  3. Never add require-dev to the root composer.json. composer update --no-dev still resolves dev requirements in order to write a lock file, so a single entry there makes that same offline resolution fail. Dev tooling lives in its own composer project under tools/ — see tools/README.md.

Regenerating the stubs

make build

That is the whole flow, and it is: check out the pinned submodules → build the compiler image from the submodule, tagged with the compiler version (ondewo-php-proto-compiler:5.15.5, never latest, which other ONDEWO clients on the same docker host would overwrite with a different compiler) → run it over the protos → write the client version into composer.json.

The generation step is a single container run, which make generate_ondewo_protos assembles like this:

mkdir -p build/proto-compiler/output && cp -R auth build/proto-compiler/output/auth
docker run --rm --user "$(id -u):$(id -g)" \
  -e HOME=/tmp -e TEMP_SRC_DIRECTORY=/tmp/compile -e COMPOSER_HOME=/tmp/composer-home -e COMPOSER_CACHE_READ_ONLY=1 \
  -v "$(pwd)/ondewo-sip-api":/input-volume/ondewo-sip-api:ro \
  -v "$(pwd)/composer.json":/input-volume/composer.json:ro \
  -v "$(pwd)/build/proto-compiler/output":/output-volume \
  ondewo-php-proto-compiler:5.15.5 ondewo-sip-api ondewo
# then: src/ and composer.json are copied back from build/proto-compiler/output
  • The two positional arguments are <relative_protos_dir> <target_subdir>: the proto root inside the input volume (which becomes protoc's -I root), and the sub-directory to scope generation to. ondewo keeps the vendored google/ tree out of the entry set while the image's dependency resolver still pulls in the google protos that are actually imported.
  • There is no -it. It breaks every non-interactive caller with cannot attach stdin to a TTY-enabled container because stdin is not a terminal.
  • The container runs as your user, so everything it writes is yours — no root-owned files and no sudo. The -e overrides move the image's scratch copy and composer home out of the root-owned /image-data, and let composer read the offline cache the image pre-warmed without writing to it.
  • The repository root is not mounted. The input volume gets exactly the proto root (read-only) and composer.json (which the image merges with its defaults); the output volume is a scratch directory with a copy of auth/ (the image adds auth/ to the shipped classmap only when it finds it there). make copies src/ (replaced wholesale, so a renamed or deleted proto leaves no orphaned stub) and the merged composer.json back; the compiler's vendor/ and composer.lock are discarded — make install_dependencies resolves its own.

To poke around inside the image, and only there, -it is correct:

docker run -it --entrypoint /bin/bash ondewo-php-proto-compiler:5.15.5

Usage

<?php

require __DIR__ . '/vendor/autoload.php';

use Grpc\ChannelCredentials;
use Ondewo\Sip\Auth\BearerTokenAuthenticator;
use Ondewo\Sip\SipClient;
use Ondewo\Sip\SipStartSessionRequest;

// The PHP namespace is protoc's UpperCamel form of the proto package:
// `package ondewo.sip;` becomes `Ondewo\Sip`, and the service `Sip` becomes
// `SipClient` (grpc_php_plugin's default class suffix). Browse src/Ondewo/Sip for
// the messages your pinned API version actually declares.
//
// BearerTokenAuthenticator is the hand-written half: it builds the `$opts` array and
// stamps `authorization: Bearer <token>` onto the metadata of every call.
$auth = new BearerTokenAuthenticator(getenv('ONDEWO_TOKEN'));
$client = new SipClient(
    getenv('ONDEWO_SIP_HOST') ?: 'localhost:50055',
    $auth->channelOptions(ChannelCredentials::createSsl())
);

$request = new SipStartSessionRequest();
$request->setAccountName('sip:4242@ondewo.com');
$request->setAutoAnswerInterval(5);

[$response, $status] = $client->SipStartSession($request)->wait();

if ($status->code !== \Grpc\STATUS_OK) {
    throw new RuntimeException("gRPC call failed ({$status->code}): {$status->details}");
}

echo $response->serializeToJsonString(), PHP_EOL;

For an insecure channel against a local server, call $auth->channelOptions() with no argument — null credentials is exactly what ChannelCredentials::createInsecure() returns.

TLS, mutual TLS and certificates

gRPC encrypts with TLS. "SSL" in names such as \Grpc\ChannelCredentials::createSsl() or grpc.ssl_target_name_override is legacy naming; no SSL protocol version is ever negotiated.

Ondewo\Sip\Auth\ClientConfig (hand-written, in auth/) builds the target and the $opts array of every generated <Service>Client, with the same TLS rules and channel defaults as the other ONDEWO SDKs:

Mode ClientConfig arguments
Plaintext (not for production) useSecureChannel: false
TLS, system trust store none (grpcCert empty)
TLS, custom CA grpcCert = PEM of the CA that signed the server certificate
Mutual TLS grpcCert plus grpcClientCert and grpcClientKey
<?php

use Ondewo\Sip\Auth\BearerTokenAuthenticator;
use Ondewo\Sip\Auth\ClientConfig;
use Ondewo\Sip\SipClient;

$config = new ClientConfig(
    host: '10.0.0.5',
    port: 50051,
    grpcCert: file_get_contents('certs/ca.pem'),
    grpcClientCert: file_get_contents('certs/client.pem'), // leave both out for server-authenticated TLS
    grpcClientKey: file_get_contents('certs/client.key'),
);
$client = new SipClient($config->target(), $config->channelOptions([
    'update_metadata' => new BearerTokenAuthenticator(getenv('ONDEWO_TOKEN')),
    'grpc.ssl_target_name_override' => 'sip.example.internal', // only when connecting by IP
]));

Rules the code enforces:

  • The three certificate fields hold PEM content, not file paths. Read the files yourself (file_get_contents() returns false for a missing file, which declare(strict_types=1) code rejects with a TypeError). CRLF line endings are fine.
  • grpcClientCert and grpcClientKey go together: setting only one throws InvalidArgumentException when the config is built, and channelCredentials() repeats the check before anything reaches ext-grpc. This is not pedantry: grpc-core aborts the whole PHP process on a private key without its certificate and silently connects without a client identity on a certificate without its key. Empty strings on both mean server-authenticated TLS; an empty grpcCert means the platform's default trust store.
  • useSecureChannel: false with a client certificate throws InvalidArgumentException instead of silently dropping the identity. A plaintext channel is allowed, but logs a warning naming host:port: on the PSR-3 logger passed to channelOptions(logger: ...) / channelCredentials($logger), otherwise through error_log(). The SDK never configures your logging.
  • channelOptions() refuses a credentials option of its own: TLS material goes through ClientConfig, where it is checked. Every other option (update_metadata, any grpc.* channel argument) is passed through and wins over the defaults.
  • No exception message renders a PEM or the config: they name the field and host:port.
  • target() brackets a bare IPv6 literal (::1 becomes [::1]:50051) and leaves [...] or scheme: hosts alone.
  • The server certificate is verified against grpcCert, and the host you connect to must match one of the certificate's subject alternative names (SAN). When you connect by IP and the certificate has no IP SAN, tell gRPC which name to check with the channel option grpc.ssl_target_name_override.

Channel defaults (ClientConfig::DEFAULT_CHANNEL_OPTIONS, identical to ondewo-client-utils-python): message size limit 2³¹-1 bytes both ways; grpc.keepalive_time_ms 30000 with grpc.keepalive_permit_without_calls 0 and grpc.http2.max_pings_without_data 2 (pings only during calls; a server answers endless pings on a silent stream with GOAWAY too_many_pings); grpc.keepalive_timeout_ms and grpc.http2.ping_timeout_ms 20000 (a dead connection is detected in about 40 s instead of 85 s); grpc.max_reconnect_backoff_ms 5000 (gRPC's default is 120 s). Gap: the Python SDK also installs a per-method retry policy (idempotent reads only); this PHP SDK installs none, so only gRPC's transparent retries (a request that provably never reached the server) apply.

A test PKI with openssl

A CA, a server certificate with SANs, and a client certificate with the clientAuth extended key usage. For tests only: the keys are unencrypted.

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 365 \
  -subj "/CN=Test CA" -keyout ca.key -out ca.pem

printf 'subjectAltName=DNS:localhost,IP:127.0.0.1\nextendedKeyUsage=serverAuth\n' > server.ext
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
  -subj "/CN=localhost" -keyout server.key -out server.csr
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 365 \
  -extfile server.ext -out server.pem

printf 'extendedKeyUsage=clientAuth\n' > client.ext
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
  -subj "/CN=my-client" -keyout client.key -out client.csr
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 365 \
  -extfile client.ext -out client.pem

chmod 600 *.key
openssl verify -CAfile ca.pem server.pem client.pem

The client then uses ca.pem / client.pem / client.key; a server that requires client certificates uses server.pem / server.key and trusts ca.pem for its clients.

TLS security notes

  • (string) $config, var_dump() / print_r() (__debugInfo()) and json_encode() (jsonSerialize()) show a non-empty grpcClientKey as ***REDACTED*** (an empty one stays empty). That JSON is for logs and cannot be read back; keep the key in a file or secret store and pass it to the constructor at startup.
  • serialize(), var_export() and an (array) cast are not redacted: they carry the private key in clear text. Do not log them, and treat anything serialized from a config as a secret (file mode 0600, never committed).
  • The key parameter is #[\SensitiveParameter]: from PHP 8.2 on, stack traces show it as a SensitiveParameterValue. PHP 8.1 ignores the attribute; there, set zend.exception_ignore_args = On (the php.ini-production default) to keep arguments out of traces.

TLS troubleshooting

A failed handshake ends the call with $status->code === \Grpc\STATUS_UNAVAILABLE (14); the cause is in $status->details:

  • CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate (inside "Ssl handshake failed"): grpcCert is not the CA that issued the server certificate (or is empty, so the system trust store is used), or the server does not send its intermediate certificates.
  • Hostname Verification Check failed (older gRPC: Peer name <host> is not in peer certificate): the host you connect to is not in the server certificate's SAN. Connect by a name in the SAN, add the SAN, or set grpc.ssl_target_name_override.
  • Socket closed against a server that requires client certificates: no client certificate was presented, or one the server's CA did not issue. The server log names the reason (e.g. PEER_DID_NOT_RETURN_A_CERTIFICATE). Set grpcClientCert / grpcClientKey.
  • empty address list, with Could not load any root certificate in the gRPC log: grpcCert holds something that is not PEM, typically a file path. Pass file_get_contents(...) instead.

Testing

make ci        # what GitHub Actions' php jobs run: no submodules, no docker
make test      # the same, plus `make check_build` against the ondewo-sip-api submodule

make check_build compares the committed stubs with the pinned ondewo-sip-api: every .proto has generated code, and the rpcs the .proto files declare are exactly the method paths the *Client.php stubs call (tools/check-generated-rpcs.pl), so a stub that is stale against the api fails. GitHub Actions runs it in its own stubs job, which fetches only that submodule.

make ci is composer validate → the credential-free Packagist dry run → php -l over the hand-written sources → PHPUnit with coverage → the coverage threshold gate. It runs on PHP 8.1 and 8.4 in GitHub Actions, against the committed stubs: no docker image is built and no proto compiler runs there.

What the suite actually asserts:

Test What it would catch
tests/Generated/GeneratedCodeTest.php A stub that does not load, a GPBMetadata descriptor whose initOnce() chain has a missing transitive import, a service whose client class was never generated, an empty src/
tests/Generated/MessageSerializationTest.php A field that never reaches the wire, a sub-message that loses its presence bit, a moved enum zero constant, a broken JSON mapping (including the integer path, which needs ext-bcmath)
tests/Generated/ServiceSurfaceTest.php A service or rpc the generated descriptors declare without a callable stub, a streaming RPC generated with the wrong shape, a stub no descriptor declares. The expectations are derived from the GPBMetadata descriptors (GeneratedServices), not listed — only a few "must contain" pins remain
tests/Generated/ServiceClientTest.php A stub that cannot be constructed against a channel, with plain or bearer-token options — for every service the descriptors declare
tests/Auth/BearerTokenAuthenticatorTest.php Any regression in the hand-written auth surface
tests/Auth/ClientConfigTest.php Half a client identity reaching ext-grpc, an identity dropped on a plaintext channel, a PEM or key in a message or rendering, a wrong target or channel default
tests/Tls/MutualTlsHandshakeTest.php A broken real handshake: TLS, mutual TLS, CRLF PEMs and [::1] must connect; a missing or foreign client certificate, a wrong CA and the system roots against the test CA must fail as UNAVAILABLE
tests/ReleaseNotesTest.php A RELEASE.md heading or separator the release-notes slice would miss (a GitHub release with empty notes), and an unanchored slice that takes the 9.1.10 entry for 9.1.1
tests/ReleaseProcessTest.php The release contract: the step order of make release, the devops-accounts clone deleted before it starts, read-only preflight/verify, no sudo, no gh auth login, no moving image tags
tests/ReleaseCredentialsHygieneTest.php A credential on a command line (make, docker, curl, gh) or interpolated into a workflow run: line

The handshake test generates its PKI with ext-openssl at test time and needs python3 with grpcio for its server (tests/Tls/tls_test_server.py; PHP has no gRPC server): pip install grpcio, or point ONDEWO_TLS_TEST_PYTHON at an interpreter that has it. CI installs it.

Coverage is reported for the hand-written code only — phpunit.xml.dist's <source> is auth/, and make coverage fails below COVERAGE_MIN (100%). The generated stubs are machine output and are deliberately outside the metric, but they are not outside the tests: every one of the committed classes is loaded and every descriptor initialised by GeneratedCodeTest.

Versioning and releasing

ONDEWO_SIP_VERSION at the top of the Makefile is the single source of truth and must match the ONDEWO SIP API in major and minor version. make update_composer_version (part of make build) propagates it into composer.json; never edit that field by hand.

# bump ONDEWO_SIP_VERSION and the submodule pins, add a RELEASE.md entry, then:
make ondewo_release

What a release does

Step Target Side effects
1 spc none — fails if release/<V> or tag <V> exists locally or on origin (git ls-remote --exit-code, exact refs), or Packagist already serves <V>
2 ondewo_release clones ondewo-devops-accounts into a private mktemp -d directory outside the repository, reads the three credentials, and deletes the clone before the release starts (a trap deletes it on any failure too). With DEVOPS_ACCOUNT_DIR=<dir> (ondewo-sip-api's driver passes its single clone) nothing is cloned or deleted
3 release_preflight none — host tools; no devops clone inside the repository; spc; a non-empty RELEASE.md entry under exactly ## Release ONDEWO SIP PHP Client <V>; GitHub accepts GITHUB_GH_TOKEN and it may push here; Packagist accepts PACKAGIST_USERNAME/PACKAGIST_API_TOKEN and that account maintains the package (see below)
4 build submodules at their pins → compiler image → stubs → composer.json version
5 precommit_hooks_run_all_files none on success; a failing hook stops the release
6 install_dependencies install_dev_tools test the full suite: composer validate, the packaging dry run, php -l, check_build (incl. the rpc drift check) and PHPUnit with the coverage gate
7 git commit the local PREPARING FOR RELEASE <V> commit (a failing commit stops the release)
8 git push, create_release_branch, create_release_tag Packagist serves the tag, so it has to exist before the publish step
9 publish the Packagist update ping (release.yml pings again after re-testing the tag)
10 push_to_gh the GitHub release, notes sliced out of RELEASE.md (refuses empty notes)
11 release_verify none — the tag is on origin, the GitHub release has a non-empty body, the tag's release.yml run succeeded, and https://repo.packagist.org/p2/ondewo/sip-client-php.json lists <V>; polled VERIFY_ATTEMPTS (20) × VERIFY_INTERVAL_S (30 s)

make -n ondewo_release and make -n release print this plan without cloning or pushing anything. release_preflight and release_verify can be run on their own at any time; both only read. Run standalone with DEVOPS_ACCOUNT_DIR=<absolute path of a devops-accounts clone> (as ondewo-sip-api's driver does), they load each credential they need from that directory when the environment does not already carry it — into their own shell only, never onto a command line or into the log.

Credentials reach only the steps that use them. There is no bare export in the Makefile: the credentials travel in the environment ondewo_release hands to make release, every step that needs none of them (build, pre-commit, composer, phpunit, git) runs under env -u … (WITHOUT_CREDENTIALS), and gh gets the token per call as GH_TOKEN=… gh … — nothing runs gh auth login, so no token is left in ~/.config/gh afterwards.

Host requirements: docker, make, git, perl (with the core module JSON::PP) and curl. Everything else comes from a pinned image when the host lacks it: php and composer from tools/php.Dockerfile, gh (2.100.0) and pre-commit (4.6.2) from tools/release-tools.Dockerfile. All containers run as the invoking user, so no step needs sudo.

What the Packagist preflight proves, without changing anything on Packagist: it POSTs to the update API with the real credentials but a repository url that matches no package. Packagist authenticates first — 403 Missing or invalid username/apiToken for bad credentials — and then answers 404 Could not find a package, so nothing is crawled. A second, public read of https://packagist.org/packages/ondewo/sip-client-php.json checks that the account is a maintainer of the package, which is what the real ping needs.

Publishing to Packagist

Packagist takes no upload. There is no twine upload and no npm publish equivalent: Packagist reads the git tags of this repository off GitHub and serves each tagged tree as a version. So "publishing" is two things, and only the second one is a command:

  1. the git tag — make create_release_tag pushes it, and that tag is the artifact;
  2. a ping that tells Packagist to crawl the repository now instead of at its next scheduled pass — make publish.
make publish    # validate the package, then POST to https://packagist.org/api/update-package

make publish refuses to run without credentials, then runs the full dry run (without the credentials in its environment), then pings the API and fails on anything but an HTTP 200/202 carrying {"status":"success"}. It is wired into make release, so a normal make ondewo_release needs no extra step — and make release_verify then waits until Packagist's p2 metadata actually lists the version.

One-time setup (a human, once per package)

None of the automation can do these; they need a browser and ownership of the ONDEWO organisation.

  1. Submit the package once. Log in to packagist.org with the ONDEWO account and use Submit with the repository URL https://github.com/ondewo/ondewo-sip-client-php. Packagist reads composer.json and claims the vendor namespace ondewo/ for that account. Until this is done the update API answers 404 for the package and make publish fails — deliberately.
  2. Install the GitHub service hook (optional but recommended) so a tag push updates Packagist even when a release is made by hand: on Packagist the package page offers the hook URL and token, or use GitHub Settings → Webhooks with payload URL https://packagist.org/api/github?username=<PACKAGIST_USERNAME>, content type application/json, secret = the Packagist API token, event Just the push event. The hook and make publish do the same job; having both is harmless, having neither means a release is only visible after Packagist's own crawl.
  3. Store the credentials — see the table below.

Credentials

Name What it is Where it lives
PACKAGIST_USERNAME the Packagist login name that owns the ondewo/ vendor namespace ondewo-devops-accounts → account_packagist.env, and the GitHub repository secret of the same name
PACKAGIST_API_TOKEN that account's API token, from packagist.org/profile → Show API token ondewo-devops-accounts → account_packagist.env, and the GitHub repository secret of the same name
GITHUB_GH_TOKEN the token gh release create authenticates with ondewo-devops-accounts → account_github.env (already in use)

All three are only ever supplied at runtime, through the environment — make ondewo_release reads them out of the devops-accounts clone (and deletes the clone before the release starts), the release workflow out of GitHub secrets. Every recipe that carries one is @-prefixed so it never reaches a build log, and make TEST prints <set> / <unset> rather than the value.

What is verified without credentials

make packagist_dry_run is the credential-free half of make publish and runs in CI on every push (.github/workflows/ci.yml, and make ci locally):

Check What it would catch
composer validate a composer.json Packagist cannot parse
composer_validate_strict any new strict-mode warning — the three deliberate ones (the version field and the two exact google/protobuf / grpc/grpc pins) are enumerated in the Makefile and allowed, everything else fails
check_version_agreement ONDEWO_SIP_VERSION, composer.json's name and version, the RELEASE.md entry and — on a tag — the tag name drifting apart. A tag that disagrees with the version field makes Packagist publish the tree under a version nobody tagged
check_packagist_payload the update request pointing at the wrong repository — the API identifies the package by its VCS url, not by its composer name

.github/workflows/release.yml runs on a X.Y.Z tag push and does the same checks plus the full test suite before it pings the API. If either PACKAGIST_* repository secret is missing, the run does not go red: the tag is still tested, the ping is skipped, and the run ends with a ::warning:: and a job-summary section naming the missing secret. The ping make release sent with the devops-accounts credentials is the authoritative one, and make release_verify checks the outcome where it matters — Packagist's p2 metadata — so a missing secret only removes a redundant second ping. Never an unauthenticated request.

See RELEASE.md for the release history and CONTRIBUTING.md for how to contribute.

License

Apache License 2.0 — see LICENSE.