Skip to content

Commit 5963ea9

Browse files
committed
fix: Stop publishing package.json and inject the version instead
Seam\Utils\PackageVersion read package.json from the package root at runtime to set the seam-sdk-version header, which forced a development manifest into every published package. Replace it with a VERSION constant, and inject that constant from package.json in the version lifecycle script, which npm runs after the bump but before the commit, so the value is part of the tagged commit. Packagist publishes the tag with no build step in between, so unlike the JavaScript SDK, which injects at pack time, the version has to be committed before the tag, as the Ruby SDK does with lib/seam/version.rb. Add export-ignore rules to .gitattributes so development files are kept out of the archives Composer downloads as dist, which composer.json's archive exclude list does not govern, and exclude package.json from composer archive as well. Cover the constant with tests asserting it matches package.json and is what SeamClient sends, so a hand edit or a missed injection fails. Removes Seam\Utils\PackageVersionException, which only signalled an unreadable package.json and has no remaining purpose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFK47311nWtbd3gHmjdknR
1 parent 0425136 commit 5963ea9

8 files changed

Lines changed: 133 additions & 41 deletions

File tree

.gitattributes

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
11
src/Resources/*.php linguist-generated
22
src/Routes/*.php linguist-generated
33
src/SeamClient.php linguist-generated
4+
5+
# Keep development files out of the published package.
6+
# GitHub builds the archives Composer downloads as dist with git archive,
7+
# which honours these rules.
8+
/.devcontainer export-ignore
9+
/.editorconfig export-ignore
10+
/.env.example export-ignore
11+
/.github export-ignore
12+
/.gitattributes export-ignore
13+
/.gitignore export-ignore
14+
/.npmrc export-ignore
15+
/.prettierignore export-ignore
16+
/.prettierrc.json export-ignore
17+
/.releaserc.json export-ignore
18+
/codegen export-ignore
19+
/eslint.config.ts export-ignore
20+
/inject-version.ts export-ignore
21+
/package-lock.json export-ignore
22+
/package.json export-ignore
23+
/phpunit.xml.dist export-ignore
24+
/tests export-ignore
25+
/tsconfig.json export-ignore

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -234,15 +234,25 @@ and dispatches the [Version](.github/workflows/version.yml) workflow.
234234
Run the [Version](.github/workflows/version.yml) workflow with the
235235
version to cut.
236236
It runs `npm version`, which bumps the `version` field in `package.json`,
237-
creates a signed `v*` git tag and pushes it.
237+
injects that version into `Seam\Utils\PackageVersion`, creates a signed `v*`
238+
git tag and pushes it.
238239
Pushing the tag triggers the [Publish](.github/workflows/publish.yml)
239240
workflow, and [Packagist](https://packagist.org/packages/seamapi/seam)
240241
picks up the new tag from its GitHub webhook.
241242

242243
> Composer has no canonical place to store a package version, since Packagist
243-
> derives it from the git tag.
244-
> This repository therefore keeps the version in `package.json` and lets
245-
> `npm version` manage the tag.
246-
> The SDK also reads that field at runtime via `Seam\Utils\PackageVersion`
247-
> to set its `seam-sdk-version` header, so `package.json` must remain in the
248-
> published package.
244+
> derives it from the git tag, and it publishes the tag as-is with no build
245+
> step in between.
246+
> This repository therefore keeps the version in `package.json`, which is a
247+
> development manifest that is not published, and injects it into the
248+
> `Seam\Utils\PackageVersion::VERSION` constant used for the
249+
> `seam-sdk-version` header.
250+
>
251+
> The injection runs from `inject-version.ts` in the `version` lifecycle
252+
> script, which npm runs after the bump but before the commit, so the updated
253+
> constant is part of the tagged commit.
254+
> Never edit that constant by hand; a test asserts it matches `package.json`.
255+
256+
Development files are kept out of the published package with `export-ignore`
257+
rules in `.gitattributes`, which `git archive` honours when GitHub builds the
258+
archives Composer downloads as `dist`.

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@
6969
"/.prettierrc.json",
7070
"/.releaserc.json",
7171
"/eslint.config.ts",
72+
"/inject-version.ts",
7273
"/package-lock.json",
74+
"/package.json",
7375
"/phpunit.xml.dist",
7476
"/tsconfig.json"
7577
]

inject-version.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { readFile, writeFile } from 'node:fs/promises'
2+
import { fileURLToPath } from 'node:url'
3+
4+
const versionFile = './src/Utils/PackageVersion.php'
5+
6+
const versionPattern = /public const VERSION = "[^"]*";/
7+
8+
const main = async (): Promise<void> => {
9+
const version = await injectVersion(
10+
fileURLToPath(new URL(versionFile, import.meta.url)),
11+
)
12+
// eslint-disable-next-line no-console
13+
console.log(`✓ Version ${version} injected into ${versionFile}`)
14+
}
15+
16+
const injectVersion = async (path: string): Promise<string> => {
17+
const { version } = await readPackageJson()
18+
19+
if (version == null) {
20+
throw new Error('Missing version in package.json')
21+
}
22+
23+
const data = (await readFile(path)).toString()
24+
25+
if (!versionPattern.test(data)) {
26+
throw new Error(`Could not find the version constant in ${versionFile}`)
27+
}
28+
29+
await writeFile(
30+
path,
31+
data.replace(versionPattern, `public const VERSION = "${version}";`),
32+
)
33+
34+
return version
35+
}
36+
37+
const readPackageJson = async (): Promise<{ version?: string }> => {
38+
const pkgBuff = await readFile(
39+
fileURLToPath(new URL('package.json', import.meta.url)),
40+
)
41+
return JSON.parse(pkgBuff.toString())
42+
}
43+
44+
await main()

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"private": true,
66
"license": "MIT",
77
"scripts": {
8+
"version": "tsx ./inject-version.ts && git add src/Utils/PackageVersion.php",
89
"postversion": "git push --follow-tags",
910
"generate": "tsx codegen/smith.ts",
1011
"postgenerate": "npm run format",

src/Utils/PackageVersion.php

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,18 @@
22

33
namespace Seam\Utils;
44

5-
class PackageVersionException extends \Exception {}
6-
75
class PackageVersion
86
{
9-
public static function get()
7+
/**
8+
* The version of this package.
9+
*
10+
* Injected from package.json when a version is cut, by the version
11+
* lifecycle script in package.json. Do not edit by hand.
12+
*/
13+
public const VERSION = "3.5.0";
14+
15+
public static function get(): string
1016
{
11-
$filePath = __DIR__ . "/../../package.json";
12-
13-
if (!file_exists($filePath)) {
14-
throw new PackageVersionException(
15-
"Can't get package version. File package.json does not exist.",
16-
);
17-
}
18-
19-
$content = file_get_contents($filePath);
20-
if ($content === false) {
21-
throw new PackageVersionException(
22-
"Unable to read package.json file to get package version.",
23-
);
24-
}
25-
26-
$json = json_decode($content, true);
27-
if (json_last_error() !== JSON_ERROR_NONE) {
28-
throw new PackageVersionException(
29-
"JSON decode error occurred when decoding package.json: " .
30-
json_last_error_msg(),
31-
);
32-
}
33-
34-
if (!isset($json["version"])) {
35-
throw new PackageVersionException(
36-
"Version not set in package.json",
37-
);
38-
}
39-
40-
return $json["version"];
17+
return self::VERSION;
4118
}
4219
}

tests/PackageVersionTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Seam\Utils\PackageVersion;
7+
8+
final class PackageVersionTest extends TestCase
9+
{
10+
public function testVersionMatchesPackageJson(): void
11+
{
12+
$path = __DIR__ . "/../package.json";
13+
$contents = file_get_contents($path);
14+
$this->assertIsString($contents, "Could not read $path");
15+
16+
$package = json_decode($contents, true);
17+
$this->assertIsArray($package, "Could not decode $path");
18+
$this->assertArrayHasKey("version", $package);
19+
20+
$this->assertSame(
21+
$package["version"],
22+
PackageVersion::get(),
23+
"Seam\\Utils\\PackageVersion is out of date with package.json. " .
24+
"It is injected by the version lifecycle script when a " .
25+
"version is cut and should not be edited by hand.",
26+
);
27+
}
28+
29+
public function testVersionIsUsedAsTheSdkVersionHeader(): void
30+
{
31+
$seam = new \Seam\SeamClient("seam_apikey1_token");
32+
$headers = $seam->client->getConfig("headers");
33+
34+
$this->assertSame(PackageVersion::get(), $headers["seam-sdk-version"]);
35+
}
36+
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"$schema": "https://json.schemastore.org/tsconfig.json",
33
"extends": "./node_modules/@seamapi/smith/tsconfig.base.json",
44
"files": ["codegen/index.ts"],
5-
"include": ["codegen/**/*", "eslint.config.ts"]
5+
"include": ["codegen/**/*", "eslint.config.ts", "inject-version.ts"]
66
}

0 commit comments

Comments
 (0)