Skip to content

Give each package its own version and release only the packages that changed - #37

Draft
jwrosewell wants to merge 8 commits into
rcfrom
ci/package-versions
Draft

jwrosewell wants to merge 8 commits into
rcfrom
ci/package-versions

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Step 3.4 of common-ci#306. On rc today, ci/get-next-package-version.ps1 makes one plain tag for the whole repository, such as 4.5.207, and the Tag job of mirror.yml puts it on every mirror, so every package is released with the same number whether it changed or not. Decisions 1 to 3 of common-ci#306 say each package has its own version, tagged <package>/<version>, and that a package is released when a file in its folder changed since its last tag, together with every package that depends on it.

How the mirrors and Packagist work

  1. Packagist reads the six mirrors, not this repository. pipeline-php-core, pipeline-php-engines, pipeline-php-cloudrequestengine, pipeline-php-did, device-detection-php and location-php are the repositories of fiftyone.pipeline.core, .engines, .cloudrequestengine, .did, fiftyone.devicedetection and fiftyone.geolocation. fiftyone.pipeline.translation, fiftyone.ipintelligence and the on-premise Device Detection package are not on Packagist.
  2. On a push to main, the Branch job of mirror.yml runs git subtree split for each mirrored folder, after ci/prepare-split.sh has cut the import merges, and pushes the result to the mirror's main.
  3. Each mirror has a webhook to Packagist, which makes a version from each tag on the mirror that is a version number, with the composer.json and GitHub's archive of the tagged commit. The commit need not be on the mirror's main.
  4. A tag such as fiftyone.pipeline.core/4.5.208 is not a version number, so it stays here, and the mirrors keep plain version tags.

What changed

  • ci/packages.json lists the nine packages with Name, Path, Ignore, DependsOn and Publish, which common-ci's steps/release-plan.ps1 reads, and the fields ci/packages.ps1 already had (Mirror, Vendor, Ci, Assembled, Native). Name is the Composer name without 51degrees/. The six packages with a mirror are published and the other three are not, as today. ci/packages.ps1 reads the list from there in place of its own table, so the build and test scripts see the same fields as before.
  • ci/packages.ps1 gains Get-ReleasePlanScript, which finds common-ci's steps/release-plan.ps1, Get-PublishedComposerJson, which gives a package's composer.json with each requirement on another package here set to ^<version> and stops when the 51degrees/ requirements and DependsOn disagree, and Get-MirrorHighestVersion, which reads the highest version tag on a public mirror.
  • ci/get-next-package-version.ps1 gives the run the highest planned version as its label.
  • ci/package-update-required.ps1 prints the plan and the requirements each released package will be published with. It then stops the publish, before anything is tagged, when a planned version is not above the highest version already on that package's mirror (lines 54 to 69). ci/mirror-tags.ps1 refuses such a version too, but only after ci/publish-package.ps1 has tagged this repository and made the GitHub releases.
  • ci/check-version-bump.ps1 and ci/check-release-interval.ps1 run common-ci's shared checks with -TagPrefix for every planned package. common-ci's nightly-publish.yml runs them in place of its own, because 51degrees-php is in its OWN_CHECK list (line 142).
  • ci/publish-package.ps1 tags each planned package <package>/<version> in the order of the plan, pushes one tag at a time and makes a GitHub release for each, then sets SkipUpdateTag so no plain tag is made. GitHub creates no push event when more than three tags are pushed at once, so one push of every tag would start no workflow.
  • ci/mirror-tags.ps1 holds what the Tag job did. For every package tag on the checked out commit it splits the folder, and when the package requires others it makes a commit on top of the split that changes only those requirements, to the newest version of each tagged on or before that commit. That commit has fixed names and the tagged commit's date, so a second run makes the same commit. It checks every mirror before tagging any, then pushes <version> to each mirror in the order of packages.json and stops at the first failure. Without -Push it only prints what it would push.
  • mirror.yml starts on */<version> tags in place of plain ones and runs ci/mirror-tags.ps1, and did-publish.yml starts on fiftyone.pipeline.did/<version>. A plain version tag no longer starts anything.
  • ci/release-plan-filter.ps1 leaves out a composer.json whose only changes are to description, keywords, homepage, support, authors or license, and each package's Ignore list leaves out its development files. See "The release rule" below.
  • publish.yml has an allow-early-release input and passes it to common-ci, which now accepts it (line 28 of nightly-publish.yml). The weekly check's refusal tells the person to set it.
  • A new Release plan workflow runs on every pull request and on pushes to main and rc. It runs the 21 Pester tests of ci/release-plan-filter.Tests.ps1, which no workflow ran before, and prints the plan a release of the commit would publish. It fails when the plan cannot be worked out or gives a version a mirror already has, so that shows on the pull request and not at the publish.
  • rc at a074012f is merged in with a merge commit (5fefc52). The one conflict was ci/packages.ps1, where rc had set Ci true for pipeline/translation. The branch keeps packages.json and sets that field there.
  • The README, AGENTS.md and the header of nightly-pipeline.yml describe the scheme.

No package folder changes, and the cxx submodule is not touched.

The release rule

James Rosewell decided on 7 November 2026 that a package is not released when the only changes under its folders since its last tag are to documentation, tests, examples or manifest metadata such as links. common-ci's steps/release-plan.ps1 leaves out **/*.md, **/docs/**, **/tests/**, **/test/**, **/examples/** and **/LICENSE*, ignoring case, and then calls ci/release-plan-filter.ps1 here with the files of each package that are left. Each package's Ignore list leaves out <path>/.github/workflows/**, <path>/ci/**, <path>/**/.gitignore, <path>/.php-cs-fixer*.php, <path>/phpstan.neon.dist and <path>/phpunit.xml. pipeline/did keeps <path>/ci/**, because pipeline/did/ci/assemble-package.sh and pipeline/did/ci/rewrite-composer-json.php build the published package.

These scripts are unchanged since run 42417106471 on 798ff86, which asserted the plan for 25 single changes made on the 4.5.206 commit. A readme, a test, an example, a docs file, a licence, composer.json metadata, or any of the seven development files of a package each planned nothing. A require added to pipeline/engines/composer.json planned fiftyone.pipeline.engines and its three dependants. A change to location/composer.json's autoload planned fiftyone.geolocation alone. pipeline/core/src/Utils.php planned fiftyone.pipeline.core and the four that depend on it. pipeline/did/ci/assemble-package.sh planned fiftyone.pipeline.did.

The first release from these tags

Every published package last released 4.5.207, from 59bb0081, the head of main and the commit of the plain tag 4.5.207. Each mirror's 4.5.207 tag holds the package folder at that commit. The tree of the tag equals the folder for the five plain splits, and for pipeline-php-did the assembled commit's parent b9395e0, the mirror's main, has the tree of pipeline/did. Packagist lists 4.5.207 for five packages and 4.5.203 for fiftyone.pipeline.did, whose hook is #32.

The tags here are <package>/4.5.206 on 64fc91d, from before that release. So these six tags have to be pushed before this merges, or the first plan compares with 4.5.206 and plans 4.5.207 again.

Tag here Commit
fiftyone.pipeline.core/4.5.207 59bb008
fiftyone.pipeline.engines/4.5.207 59bb008
fiftyone.pipeline.cloudrequestengine/4.5.207 59bb008
fiftyone.pipeline.did/4.5.207 59bb008
fiftyone.devicedetection/4.5.207 59bb008
fiftyone.geolocation/4.5.207 59bb008
for p in fiftyone.pipeline.core fiftyone.pipeline.engines fiftyone.pipeline.cloudrequestengine fiftyone.pipeline.did fiftyone.devicedetection fiftyone.geolocation; do
  git tag "$p/4.5.207" 59bb0081cff9c9c83b44e87aa1dd36e9b39b136d
done
git push origin $(for p in fiftyone.pipeline.core fiftyone.pipeline.engines fiftyone.pipeline.cloudrequestengine fiftyone.pipeline.did fiftyone.devicedetection fiftyone.geolocation; do echo "refs/tags/$p/4.5.207"; done)

Pushing them starts no workflow. At 59bb008, mirror.yml and did-publish.yml start only on a plain version such as 4.5.207, and six tags in one push make no push event. The nightly on main still uses the plain scheme, and 59bb008 already carries 4.5.207, so its publish finds that version present and releases nothing. The 4.5.206 package tags have sat beside plain tags since 7 November, and the nightlies of 12 and 13 November and the 4.5.207 Publish (run 43331065748) passed.

Checks

The guard against a version a mirror already has was shown red then green.

  • Red. The Release plan check on 769b7f2, before the guard (run 43474766971), passed while printing a plan of all six packages at 4.5.207, which every mirror already has, because only the 4.5.206 tags exist here.
  • Green. The same check on d90ba55 (run 43475719433) prints the same plan and then fails with "Nothing is published, because the plan gives versions already published", naming each package and mirror. That check stays red on this pull request until the six tags above are pushed, by design, and then passes on a re-run.

Two temporary branches checked the whole publish with the six 4.5.207 tags made in each job's own checkout by a temporary block in ci/packages.ps1, as they are not pushed yet. Nothing was pushed from either.

  1. tmp/php-37-plan is this head (d90ba55) with that block and a temporary workflow that lays out common-ci and this repository as the publish does, prints the plan, runs ci/publish-package.ps1 as a dry run and then ci/mirror-tags.ps1 without -Push against the real mirrors (run 43475747158). It printed "No package changed since its last release, so there is nothing to publish", as rc since 4.5.207 changed only AGENTS.md and the cxx and javascript-templates submodules, which belong to no published package. A last step found no package tag above 4.5.207 here and 4.5.207 still the highest version on all six mirrors.
  2. tmp/php-37-publish-dryrun adds one comment line to pipeline/cloudrequestengine/src/CloudRequestEngine.php, and sets PublishAnyBranch in ci/install-package.ps1 so common-ci's Package job runs ci/publish-package.ps1 off main, as a dry run. The temporary workflow (run 43475746023) printed the plan below, and ci/mirror-tags.ps1 printed for each of the three the commit it would push, whose composer.json differs from the split only in its 51degrees/ requirements, for example "51degrees/fiftyone.pipeline.engines": "4.*" to "^4.5.207" for fiftyone.pipeline.cloudrequestengine.

Publish dry run. This repository's own Publish workflow, dispatched with dryrun on tmp/php-37-publish-dryrun at 63d6ab8 (run 43475792422), passed. All 30 jobs that ran passed, including the 13 Pre Build and 13 Test jobs on Ubuntu PHP 7.4 to 8.5, Windows PHP 8.3 and macOS PHP 8.1 to 8.5, and Report Compare Performance was skipped as Compare Performance passed. Its Configure job gave the label 4.5.208 and printed this plan.

Name                                 Previous Version Reason
fiftyone.pipeline.cloudrequestengine 4.5.207  4.5.208 changed since 4.5.207 (1 file)
fiftyone.devicedetection             4.5.207  4.5.208 a package it depends on is released
fiftyone.geolocation                 4.5.207  4.5.208 a package it depends on is released
fiftyone.pipeline.cloudrequestengine 4.5.208 requires 51degrees/fiftyone.pipeline.engines ^4.5.207, 51degrees/fiftyone.pipeline.core ^4.5.207
fiftyone.devicedetection 4.5.208 requires 51degrees/fiftyone.pipeline.core ^4.5.207, 51degrees/fiftyone.pipeline.engines ^4.5.207, 51degrees/fiftyone.pipeline.cloudrequestengine ^4.5.208
fiftyone.geolocation 4.5.208 requires 51degrees/fiftyone.pipeline.core ^4.5.207, 51degrees/fiftyone.pipeline.engines ^4.5.207, 51degrees/fiftyone.pipeline.cloudrequestengine ^4.5.208

So the one changed package and the two that depend on it are released, and fiftyone.pipeline.core, fiftyone.pipeline.engines and fiftyone.pipeline.did are not. The version check passed each of the three with "Publishing 4.5.208, against 4.5.207 already released. The patch place, which needs no agreement." The weekly check named each one's last release as 12 November and said the next may go from 19 November, as a notice, because WEEKLY_RELEASES_FROM is not set. The Package job printed "Dry run - tagged '...' locally, not pushed, no release made" for the three tags in plan order, then "Tag update skipped", so no plain tag would be made.

The quick checks on d90ba55 (environment variable names, only rc into main, UTM link lint) passed. No package code changes, so this has no full build of its own, under the 12 November decision for changes to CI scripts and their notes.

Before this merges

  1. Push the six tags above.
  2. Re-run the Release plan check on this pull request, which should then print "nothing to publish" and pass.

Weekly releases

Each package released 4.5.207 on 12 November, so the next release under the weekly rule may go from 19 November. James Rosewell asked on 13 November for a release of every repository on Monday 16 November, which is his permission for that early release, so the Monday Publish is dispatched with allow-early-release.

What remains

  • A publish that stops part way. ci/publish-package.ps1 pushes and releases one package at a time in plan order. If it stops after the first tag is pushed, by a failed push or a failed gh release create, the packages pushed so far are published and the rest of the plan is not. Running Publish again does not finish it, because common-ci's Get-ReleasePlan (steps/release-plan.ps1 lines 201 to 203 and 234 to 242 at 1c5c0e0b) leaves out a package tagged at HEAD and releases a dependant only with a package that changed, so it plans nothing. The fix belongs in that shared plan, which the other per-package languages read too, and common-ci#326 tracks that fix. Until then, finish a stopped release by hand from the plan the stopped run's Configure job printed. For each planned package with no <package>/<version> tag on the released commit, in plan order, run git tag <package>/<version> <commit>, then git push origin refs/tags/<package>/<version> on its own, so that each push starts mirror.yml, then gh release create <package>/<version> --verify-tag --title "<package> <version>" --notes "<package> <version>: a package it depends on is released.". Make the missing GitHub release of any package whose tag was pushed the same way. The checks in ci/mirror-tags.ps1 still apply to a tag pushed like this.
  • #32, the Packagist hook of pipeline-php-did, which needs the Packagist account that owns the package. fiftyone.pipeline.did 4.5.204 to 4.5.207 are tagged on the mirror but not on Packagist.
  • The Tag job of mirror.yml was not run with a real push, as that would publish. The dry runs run the same ci/mirror-tags.ps1 without -Push against the real mirrors.
  • Once #87 removes the package directories' old workflows, the <path>/.github/workflows/** entries of each Ignore list match nothing. They stay, so a workflow added there by mistake still releases nothing.

…changed

Each package is now tagged here as <package>/<version>, such as
fiftyone.pipeline.core/4.5.207, and the publish releases a package only when
a file in its folder changed since its last tag, with every package that
requires it. Only the patch number moves. The plan comes from common-ci's
steps/release-plan.ps1, which reads the new ci/packages.json.

- ci/packages.json lists the packages, what each requires and where each is
  published. ci/packages.ps1 reads it in place of its own table and keeps
  the build and test fields as before.
- ci/get-next-package-version.ps1 gives the run the highest planned version
  as its label, and ci/package-update-required.ps1 prints the plan and the
  requirements each released package will be published with.
- ci/check-version-bump.ps1 and ci/check-release-interval.ps1 check each
  planned package against its own tags with common-ci's shared steps.
- ci/publish-package.ps1 tags each planned package, one tag a push, because
  GitHub starts no workflow for a push of more than three tags, and makes a
  GitHub release for each.
- ci/mirror-tags.ps1 puts each released version on its package's mirror.
  A package that requires others is published with those requirements
  raised to the versions released with it, on a commit on top of the split
  that changes only them. mirror.yml runs it for every package tag, and
  did-publish.yml now starts from the tags of pipeline/did.
- The README, AGENTS.md and the nightly pipeline describe the new scheme.

Part of step 3.4 of 51Degrees/common-ci#306.
PowerShell names ignore case, so the loop variable $push in
ci/mirror-tags.ps1 replaced the -Push switch, and the loop failed with
"Cannot convert value PSCustomObject to type SwitchParameter" once a package
was planned.
…lease plan

common-ci's release plan now leaves out Markdown files, documentation, tests, examples and licences by default, and calls ci/release-plan-filter.ps1 with the files that are left. The filter here leaves out a composer.json whose only changes are to description, keywords, homepage, support, authors or license, so a change that only moves a link releases nothing, while a change to require, autoload, version or any other field still releases the package.
Each package's Ignore list in ci/packages.json leaves out its phpunit.xml, phpstan.neon.dist, .php-cs-fixer.php, .gitignore files, .github/workflows and ci/ folder, as James decided on 7 November 2026. pipeline/did keeps its ci/ folder counting, because ci/assemble-package.sh and ci/rewrite-composer-json.php build the published fiftyone.pipeline.did.
…every pull request

The weekly check's refusal tells the person to run this repository's
Publish workflow with allow-early-release set, which common-ci's
nightly-publish.yml now accepts. The Release plan workflow runs the
release plan filter's tests and prints the plan a release of the commit
would publish, and fails when the plan cannot be worked out, such as a
composer.json requirement that DependsOn does not list.
…r already has

mirror-tags.ps1 already refuses such a version, but only after
publish-package.ps1 has tagged this repository and made the GitHub
releases. It happens when a package's last release has no
<package>/<version> tag here, as after the 4.5.207 release, which was
tagged the old way.
# Conflicts:
#	ci/packages.ps1
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