Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,13 @@ jobs:
# Prod-like image with the Symfony profiler (admin-gated). Published for
# operators to switch to on demand — never the default deployment.
- name: Build and push profiling image
# Skip on PRs entirely (#624 QW10): the profiling image is never deployed
# from a PR, so its breakage can surface on the main-push build instead.
if: github.event_name != 'pull_request'
# Build ONLY on a tag push (release) or manual dispatch. The prod hot-deploy
# runs :profiling-<sha>, and /ui/admin/status reads its version from that
# image's baked APP_BUILD_REF. Building it on main-push too made both runs
# write the same :profiling-<sha> tag (last-writer-wins), so a release image
# could end up stamped ref=main instead of its tag. Tag/dispatch-only keeps
# the deployed image's ref deterministically the release tag (e.g. v6.3.4).
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
env:
GIT_SHA: ${{ github.sha }}
Expand All @@ -105,7 +109,7 @@ jobs:
source: .
targets: app-profiling
files: docker-bake.hcl
# The step's `if:` already excludes PRs, so this only ever runs on a push.
# The step's `if:` restricts this to tag/dispatch runs, never a PR.
push: true
set: |
*.cache-from=type=gha
Expand Down
58 changes: 40 additions & 18 deletions src/Controller/Admin/GetStatusAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,7 @@
'env' => $this->kernel->getEnvironment(),
'debug' => $this->kernel->isDebug(),
'locale' => $this->param('app_locale'),
'version' => InstalledVersions::isInstalled('netresearch/timetracker')
? InstalledVersions::getPrettyVersion('netresearch/timetracker')
: null,
'version' => $this->appVersion(),
],
'build' => $this->buildInfo(),
'php' => [
Expand Down Expand Up @@ -120,32 +118,56 @@
*/
private function buildInfo(): array
{
$env = static function (string $key): ?string {
// getenv() reads the real process environment regardless of PHP's
// variables_order (where the APP_BUILD_* Docker env land); fall back
// to the superglobals for SAPIs/setups that only populate those.
$value = getenv($key);
if (false === $value || '' === $value) {
$value = $_SERVER[$key] ?? $_ENV[$key] ?? null;
}

return is_string($value) && '' !== $value ? $value : null;
};

$revision = $env('APP_BUILD_REVISION');
$ref = $env('APP_BUILD_REF');
$revision = $this->buildEnv('APP_BUILD_REVISION');
$ref = $this->buildEnv('APP_BUILD_REF');

return [
'revision' => $revision,
'ref' => $ref,
'date' => $env('APP_BUILD_DATE'),
'date' => $this->buildEnv('APP_BUILD_DATE'),
'repositoryUrl' => self::REPOSITORY_URL,
'commitUrl' => null !== $revision ? self::REPOSITORY_URL . '/commit/' . $revision : null,
'refUrl' => null !== $ref ? self::REPOSITORY_URL . '/tree/' . rawurlencode($ref) : null,
'releasesUrl' => self::REPOSITORY_URL . '/releases',
];
}

/**
* Read a build-provenance env var (APP_BUILD_*), null when unset.
*
* getenv() reads the real process environment regardless of PHP's
* variables_order (where the APP_BUILD_* Docker env land); fall back to the
* superglobals for SAPIs/setups that only populate those.
*/
private function buildEnv(string $key): ?string
{
$value = getenv($key);
if (false === $value || '' === $value) {
$value = $_SERVER[$key] ?? $_ENV[$key] ?? null;
}

return is_string($value) && '' !== $value ? $value : null;
}

/**
* The release version for the "Application" section. The profiling image is
* built only on tag pushes, so a deployed release carries its tag (e.g.
* "v6.3.4") in APP_BUILD_REF — prefer that, stripped of the leading "v".
* Falls back to Composer's root version, which reads "1.0.0+no-version-set"
* in a CI build that has no tag/VCS context.
*/
private function appVersion(): ?string
{
$ref = $this->buildEnv('APP_BUILD_REF');
if (null !== $ref && 1 === preg_match('/^v?\d+\.\d+\.\d+/', $ref)) {
return ltrim($ref, 'v');
}

return InstalledVersions::isInstalled('netresearch/timetracker')
? InstalledVersions::getPrettyVersion('netresearch/timetracker')
: null;
}

/** @return array<string, string|null> */
private function packageVersions(): array
{
Expand Down Expand Up @@ -376,7 +398,7 @@
}

/** A non-empty environment value (real env first, then superglobals), or null. */
private function env(string $key): ?string

Check warning on line 401 in src/Controller/Admin/GetStatusAction.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Update this method so that its implementation is not identical to "buildEnv" on line 142.

See more on https://sonarcloud.io/project/issues?id=netresearch_timetracker&issues=AZ9qx0BX1y6CP29KP5Fa&open=AZ9qx0BX1y6CP29KP5Fa&pullRequest=645
{
$value = getenv($key);
if (false === $value || '' === $value) {
Expand Down
24 changes: 24 additions & 0 deletions tests/Controller/GetStatusActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,30 @@ public function testSubsystemCardsCoverExpectedStorage(): void
self::assertStringNotContainsStringIgnoringCase('password', json_encode($json, JSON_THROW_ON_ERROR));
}

public function testVersionPrefersTheBuildRefTag(): void
{
// The profiling image is built only on tag pushes, so a deployed release
// carries its tag in APP_BUILD_REF. The "Application → Version" field
// surfaces it (stripped of the leading "v") rather than Composer's
// unversioned root default ("1.0.0+no-version-set").
putenv('APP_BUILD_REF=v9.9.9');
$_SERVER['APP_BUILD_REF'] = 'v9.9.9';

try {
$this->getJson('/admin/status');
$this->assertStatusCode(200);

$json = $this->getJsonResponse($this->client->getResponse());
self::assertIsArray($json['app']);
self::assertSame('9.9.9', $json['app']['version']);
self::assertIsArray($json['build']);
self::assertSame('v9.9.9', $json['build']['ref']);
} finally {
putenv('APP_BUILD_REF');
unset($_SERVER['APP_BUILD_REF']);
}
}

public function testForbiddenForNonAdmin(): void
{
$this->logInSession('developer'); // type DEV — not an admin
Expand Down
Loading