Skip to content

ci: restore CI and adopt the SDK build-and-publish standard - #931

Draft
rdelpret wants to merge 9 commits into
mainfrom
feat/ssc-publish-standard
Draft

ci: restore CI and adopt the SDK build-and-publish standard#931
rdelpret wants to merge 9 commits into
mainfrom
feat/ssc-publish-standard

Conversation

@rdelpret

@rdelpret rdelpret commented Jul 30, 2026

Copy link
Copy Markdown

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_failure in 0 seconds with no annotation:

07-29  startup_failure  Test and Deploy   (schedule)
07-28  startup_failure  Test and Deploy   (schedule)
07-23  startup_failure  Test and Deploy   (pull_request)
07-23  startup_failure  Lint PR           (pull_request_target)

Cause: the workflows predate the twilio org 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, and twilio-cli are all in the same state.

What changed

test-and-deploy.yml is replaced by ci.yml + release.yml.

Blocked actions replaced

Was Now
sendgrid/dx-automator/actions/release twilio/sdk-actions/github-release (also drops an EOL node16 runtime)
amannn/action-semantic-pull-request twilio/sdk-actions/semantic-pr-title
docker/login-action inline docker login --password-stdin

First-party twilio/* actions resolve without an allow-list entry, so these need no approval. shivammathur/setup-php is the one exception that can't be replaced with glue — it's now allow-listed at 2.37.2.

Removed — please note

  • Slack failure notifications. rtCamp/action-slack-notify isn't allow-listed; a replacement can be revisited separately.
  • SonarCloud analysis. 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.
  • 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 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 release webhook, 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 production environment gate. Previously any tag matching * went straight to a public release with no gate at all.

Other changes

  • release.yml 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
  • 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
  • Best-effort build attestation over the zipball Composer installs. Explicitly not parity with npm provenance — GitHub doesn't guarantee zipball checksum stability
  • pr-lint.yml moved to ubuntu-x64; the org doesn't serve ubuntu-latest for same-repo jobs, so that job would have hung in queued forever with no error

Two 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) changed require-dev from ranges to exact pins and committed a composer.lock whose php-cs-fixersymfony/* chain requires PHP ≥ 8.2. But composer.json still declares "php": ">=7.1.0".

Result: composer install was unsatisfiable on 5 of 8 supported PHP versions. The last fully green run (25681515425, 2026-05-11) predates #928.

Fix: require-dev goes back to ranges; composer.lock stays. The lock is what provides reproducibility, and require-dev never reaches consumers — Composer ignores a dependency's lockfile — 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.

2. The lowest/highest dependency axis never ran — and can't

Removed, with proof. Before #928, the Makefile's clean target ran rm -rf ... composer.lock, and install depends on clean. So the old workflow's sequence was:

composer update --prefer-lowest   # writes a lockfile with oldest deps
make install test                 # clean → DELETES composer.lock
                                  # composer install → resolves NEWEST again

Every lowest cell was silently testing newest deps. #928 dropped composer.lock from clean, which made the axis take effect for the first time — and it immediately 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 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 require is php itself — 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.


⚠️ Action required from the team

The cluster-test credentials are dead

All five ClusterTest cases fail HTTP 401 Authenticate. This PR did not break it: the env block is byte-for-byte the old workflow's, and the same secrets passed on 2026-05-11. The API key behind TWILIO_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 in ci.yml:

Mint a new API key on the cluster-test account, update TWILIO_CLUSTER_TEST_API_KEY + TWILIO_CLUSTER_TEST_API_KEY_SECRET, then drop continue-on-error.

The setup-php allow-list entry is not durable

It's currently a manual UI addition. The next github-org-config Terraform 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 PR will keep showing startup_failure on this PR — expected

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 cannot pass until this merges, at which point it self-heals. The fixed version is in this branch.


Verification

  • CI green across PHP 7.2–8.4 plus the lockfile-hygiene gate — run 31213803834
  • The php branch of artifactory-oidc is now proven. This was the one unverified piece: sdk-actions isn't in Artifactory's trusted_builds, so only a real consumer repo could exercise the live bearer-auth handshake. It succeeds in every job here.
  • sdk-actions side 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-actions refs now point at its merge commit on main (09631bf) rather than a feature branch. No v1.1.0 tag exists yet; each ref carries a comment to repin once it's cut. The current pins are immutable commit SHAs on main, 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

rdelpret and others added 9 commits July 28, 2026 10:11
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant