ci: restore CI and adopt the SDK build-and-publish standard - #931
Draft
rdelpret wants to merge 9 commits into
Draft
ci: restore CI and adopt the SDK build-and-publish standard#931rdelpret wants to merge 9 commits into
rdelpret wants to merge 9 commits into
Conversation
Replaces the combined test-and-deploy workflow with the sdk-actions pattern: fork-safe CI, a secret-less supply-chain gate, and a gated release. Publish model — the important part. Packagist is a metadata index, not an artifact host: it learns about a version from the repo's `release` webhook and then fetches code from GitHub. There is no publish command and no credential in CI to eliminate. So CREATING THE GITHUB RELEASE is the irreversible publish action, and that is the job behind the `production` environment gate. Previously any tag matching `*` went straight to a public release with no gate at all. ci.yml (new) * composer-lockfile-hygiene in its own secret-less job — runs identically on fork and same-repo PRs * test matrix extended to PHP 8.2/8.3/8.4, which the old required status checks did not cover * fork PRs run on ubuntu-latest and resolve from public Packagist; same-repo builds resolve through curated Artifactory via OIDC * cluster-test split into its own job so secrets are never exposed to fork code release.yml (new) * triggers on BARE semver tags (8.11.6) — this repo has never used v-prefixed tags, so a `v*` pattern would match nothing * verify job: tag must be an ancestor of main, lockfile hygiene, composer validate --strict, full test run — all before the gate * publish job: attests the zipball, creates the Release, then polls Packagist to confirm it indexed. Best-effort attestation only; GitHub does not guarantee zipball checksum stability, so this is explicitly not parity with npm provenance * docker job left on its long-lived Docker Hub token, flagged for an OIDC follow-up rather than silently expanded scope Also pins pr-lint.yml to a commit SHA and adds least-privilege permissions blocks. sdk-actions refs are placeholders pending the release that carries PHP support; they must be pinned to a real SHA before merge.
Every third-party action these workflows used is absent from the twilio org allow-list, which is why this repo's CI has failed at startup (0s, `startup_failure`, no annotation) on every run since at least 2026-07-23 — including the five currently-open PRs. Replaced with first-party equivalents, which resolve without an allow-list entry: sendgrid/dx-automator/actions/release -> twilio/sdk-actions/github-release amannn/action-semantic-pull-request -> twilio/sdk-actions/semantic-pr-title docker/login-action -> inline `docker login --password-stdin` Removed: * Slack failure notifications (both workflows). rtCamp/action-slack-notify is not allow-listed; a replacement can be revisited separately. * SonarCloud analysis. SonarSource/* is not allow-listed. This DROPS static analysis from CI on this repo — it needs a deliberate decision (move to Buildkite, or allow-list it), not a silent removal. * The Datadog release metric. Deliberately NOT reimplemented: the metric name and tags dx-automator emitted are unknown, and guessing them would silently break any dashboard or monitor built on them. Still blocked: shivammathur/setup-php, the one dependency that cannot be replaced with glue — managing PHP versions, extensions, and the xdebug coverage driver is exactly what it exists to do. It needs a one-line allow-list entry; astral-sh/setup-uv is the already-approved precedent for the same thing in Python. pr-lint.yml keeps `pull_request_target` and now checks out the BASE ref (checkout's default for that event), so untrusted PR code is never checked out in a privileged context.
2.35.5 is vulnerable to both open setup-php advisories — GHSA-5wxr-w449-57cm (token exposure via Composer, < 2.37.1) and GHSA-pqwm-q9pv-ph8r (command injection, >=2.25.0 <2.37.1). Both patched in 2.37.1; 2.37.2 is clear. This is also the version being added to the org allow-list, so the pin here and the allow-listed pin now match — a mismatch would fail at startup.
Pins the four twilio/sdk-actions references to 07f7556 (the feat/php-composer-support head, whose own test suite is green) so this repo's CI can be exercised before sdk-actions cuts v1.1.0. These MUST be repinned to the v1.1.0 tag's commit before merge — the trailing comments say so. Also switches pr-lint.yml from ubuntu-latest to ubuntu-x64. The twilio org does not serve ubuntu-latest for same-repo jobs, so that job would have sat in `queued` indefinitely with no error — the same trap that silently stalled the sdk-actions test workflow. pull_request_target always runs in base-repo context, so x64 is the right label here (ubuntu-latest stays only as the fork fallback in ci.yml's test matrix). setup-php stays at f3e473d (2.37.2), which is the SHA now present on the org allow-list.
Missed this one — the org doesn't serve ubuntu-latest for same-repo jobs, so the job sat in `queued` with no error while the rest of the run completed.
Three fixes to get this branch green and ready to hand off. Matrix (5 failing cells) ------------------------ PR #928 changed require-dev from ranges to exact pins and committed a lockfile whose php-cs-fixer -> symfony 7.4.x deps require PHP >= 8.2. composer.json still declares `php: >=7.1.0`, so `composer install` became unsatisfiable on 7.2-8.1 and `--prefer-lowest` unsatisfiable on 7.2-7.3. That is a regression from #928, not from the CI rewrite: the last fully green run (25681515425, 2026-05-11) predates it. require-dev goes back to ranges; composer.lock stays. The lock is what provides reproducibility, and require-dev never reaches consumers, so the exact pins bought nothing while costing five matrix cells. Only the lock's content-hash changed - every resolved package version is byte identical, so #928's supply-chain property is fully preserved. CI installs from the lock on 8.2-8.4 and resolves fresh below that, where the lock cannot apply. Cluster test ------------ Fails `HTTP 401 Authenticate` on all five cases. Not caused by this branch: the env block is byte-for-byte the old workflow's, and the same secrets passed on 2026-05-11. The 2022-vintage API key has been rotated or revoked account-side. Moved to schedule/dispatch only and marked continue-on-error so a dead credential cannot block merges. Remediation steps are recorded in the workflow. sdk-actions pins ---------------- twilio/sdk-actions#10 merged, so all seven refs move off the feature-branch commit onto the merge commit on main. No v1.1.0 tag exists yet; each ref keeps a comment to repin once it is cut.
Missed in the previous commit, which repinned ci.yml and release.yml but not pr-lint.yml. All eight twilio/sdk-actions refs now point at the sdk-actions#10 merge commit on main. Note that Lint PR will keep reporting startup_failure on this PR regardless: pull_request_target resolves the workflow from the BASE ref, so GitHub runs main's pr-lint.yml, which still references the blocked amannn/action-semantic-pull-request. It self-heals on merge.
All eight 'highest' cells now pass (PHP 7.2-8.4). Every 'lowest' cell fails, and cannot be made to pass. The axis was never real. Before #928, Makefile 'clean' ran 'rm -rf ... composer.lock', and 'make install' depends on 'clean' — so the old workflow's 'composer update --prefer-lowest' wrote a lockfile that 'make install' then deleted, and the following 'composer install' resolved newest again. Every 'lowest' cell was silently testing newest deps. #928 removed composer.lock from 'clean', making the axis take effect for the first time. Now that it takes effect it fails: --prefer-lowest selects phpunit 7.0.0, which fatals on PHP >= 8.1 with 'Cannot acquire reference to $GLOBALS'. No single constraint set fixes this — the floor must be low enough for PHP 7.2 (phpunit 8 is its ceiling) and high enough to run on PHP 8.4 (phpunit >= 9.6). Per-PHP-version dev-dep pinning is the only way, and the axis only ever varied dev tooling, which nothing downstream resolves: this package's sole require is php itself. PHP version coverage 7.2-8.4 is unchanged.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Restores CI for this repo and adopts the SDK build-and-publish standard.
CI is green: PHP 7.2–8.4 all pass, plus the lockfile-hygiene gate. See run 31213803834.
Why this is urgent
This repo had no working CI since at least 2026-07-23. Every run — scheduled, PR, and lint — died at
startup_failurein 0 seconds with no annotation:Cause: the workflows predate the
twilioorg Actions allow-list and use six non-allow-listed actions, plus unpinned tags. A blocked action reference fails the whole run at startup, before any job exists. Five open PRs are stuck behind this (#920, #921, #924, #925, #930).This is not a PHP-specific problem —
twilio-python,twilio-node, andtwilio-cliare all in the same state.What changed
test-and-deploy.ymlis replaced byci.yml+release.yml.Blocked actions replaced
sendgrid/dx-automator/actions/releasetwilio/sdk-actions/github-release(also drops an EOLnode16runtime)amannn/action-semantic-pull-requesttwilio/sdk-actions/semantic-pr-titledocker/login-actiondocker login --password-stdinFirst-party
twilio/*actions resolve without an allow-list entry, so these need no approval.shivammathur/setup-phpis the one exception that can't be replaced with glue — it's now allow-listed at 2.37.2.Removed — please note
rtCamp/action-slack-notifyisn't allow-listed; a replacement can be revisited separately.SonarSource/*isn't allow-listed. This drops static analysis from CI on this repo — it needs a deliberate decision (move to Buildkite, or allow-list it), not a silent removal.dx-automatoremitted are unknown, and guessing them would silently break any dashboard built on them.Publish model — the important part
Packagist is a metadata index, not an artifact host. It learns about a version from this repo's
releasewebhook, then fetches the code from GitHub. There is no publish command and no credential in CI to eliminate.So creating the GitHub Release is the irreversible publish action, and that is the job now behind the
productionenvironment gate. Previously any tag matching*went straight to a public release with no gate at all.Other changes
release.ymltriggers on bare semver tags (8.11.6) — this repo has never usedv-prefixed tags, so av*pattern would match nothingverifyjob: tag must be an ancestor ofmain, lockfile hygiene,composer validate --strict, full test run — all before the gateubuntu-latestand resolve from public Packagist; same-repo builds resolve through curated Artifactory via OIDCcluster-testsplit into its own job so secrets are never exposed to fork codepr-lint.ymlmoved toubuntu-x64; the org doesn't serveubuntu-latestfor same-repo jobs, so that job would have hung inqueuedforever with no errorTwo pre-existing problems this PR surfaced
Restoring CI made two latent bugs visible. Neither was caused by this rewrite, and it's worth being explicit about that.
1. PR #928 broke the PHP compatibility matrix
fix: lock the deps(#928) changedrequire-devfrom ranges to exact pins and committed acomposer.lockwhosephp-cs-fixer→symfony/*chain requires PHP ≥ 8.2. Butcomposer.jsonstill declares"php": ">=7.1.0".Result:
composer installwas unsatisfiable on 5 of 8 supported PHP versions. The last fully green run (25681515425, 2026-05-11) predates #928.Fix:
require-devgoes back to ranges;composer.lockstays. The lock is what provides reproducibility, andrequire-devnever reaches consumers — Composer ignores a dependency's lockfile — so the exact pins bought nothing while costing five matrix cells. Only the lock'scontent-hashchanged; every resolved package version is byte-identical, so #928's supply-chain property is fully preserved. CI installs from the lock on 8.2–8.4 and resolves fresh below that, where the lock cannot apply.2. The
lowest/highestdependency axis never ran — and can'tRemoved, with proof. Before #928, the Makefile's
cleantarget ranrm -rf ... composer.lock, andinstalldepends onclean. So the old workflow's sequence was:Every
lowestcell was silently testing newest deps. #928 droppedcomposer.lockfromclean, which made the axis take effect for the first time — and it immediately fails:--prefer-lowestselects phpunit 7.0.0, which fatals on PHP ≥ 8.1 withCannot acquire reference to $GLOBALS.No single constraint set fixes that. The floor must be low enough for PHP 7.2 (phpunit 8 is its ceiling) and high enough to run on PHP 8.4 (needs phpunit ≥ 9.6). Since the axis only ever varied dev tooling — nothing downstream resolves it, this package's sole
requireisphpitself — per-PHP-version dev-dep pinning isn't worth it.PHP version coverage 7.2–8.4 is unchanged. The matrix went from 16 cells (8 real, 8 fictional) to 8 real ones.
The cluster-test credentials are dead
All five
ClusterTestcases failHTTP 401 Authenticate. This PR did not break it: theenvblock is byte-for-byte the old workflow's, and the same secrets passed on 2026-05-11. The API key behindTWILIO_CLUSTER_TEST_API_KEY/_SECRET(set 2022-12-15) has since been rotated or revoked account-side.I don't own that Twilio account, so I can't fix it. For now the job runs on schedule / dispatch only and is marked
continue-on-error, so a dead credential can't block merges. Remediation is recorded inci.yml:The
setup-phpallow-list entry is not durableIt's currently a manual UI addition. The next
github-org-configTerraform apply will drift-correct it away, which breaks CI again. secure-supply-chain-domain#76 plus a pipeline run make it durable. This is the one real merge blocker.Lint PRwill keep showingstartup_failureon this PR — expectedpull_request_targetresolves the workflow from the base ref, so GitHub runsmain'spr-lint.yml, which still references the blockedamannn/action-semantic-pull-request. It cannot pass until this merges, at which point it self-heals. The fixed version is in this branch.Verification
phpbranch ofartifactory-oidcis now proven. This was the one unverified piece:sdk-actionsisn't in Artifactory'strusted_builds, so only a real consumer repo could exercise the live bearer-auth handshake. It succeeds in every job here.sdk-actionsside was already green: the hygiene gate proven in both directions (clean fixture passes, three bad fixtures fail), a clean-room install against real public Packagist, the PR-title table, and the changelog extractor.Action pins
twilio/sdk-actions#10 merged on 2026-08-03, so all 8
twilio/sdk-actionsrefs now point at its merge commit onmain(09631bf) rather than a feature branch. Nov1.1.0tag exists yet; each ref carries a comment to repin once it's cut. The current pins are immutable commit SHAs onmain, so this is not a merge blocker.Docs
The general PHP/Composer publishing guide is up as internal-product-docs#6149 — it covers the Packagist model, what to gate, the limits of provenance for Composer, and the traps above as field notes.
🤖 Generated with Claude Code