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
236 changes: 236 additions & 0 deletions .github/workflows/beta-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
name: Build Logic4 QA beta

on:
workflow_dispatch:
inputs:
publish:
description: Publish a beta tag and GitHub prerelease
required: true
default: false
type: boolean
base_version:
description: SemVer base such as 4.1.0; defaults to the BETA_BASE_VERSION repository variable
required: false
type: string
schedule:
- cron: '0 8 * * *'

permissions: {}

concurrency:
group: logic4-qa-beta
cancel-in-progress: true

jobs:
generate:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
base_sha: ${{ steps.generate.outputs.base_sha }}
has_changes: ${{ steps.generate.outputs.has_changes }}
qa_version: ${{ steps.generate.outputs.qa_version }}

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.3'
coverage: pcov

- name: Validate composer.json
run: composer validate --strict

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction

- name: Generate QA client
id: generate
shell: bash
run: |
set -euo pipefail

composer generate-beta-endpoints
git add --all src

echo "base_sha=${GITHUB_SHA}" >> "${GITHUB_OUTPUT}"

if git diff --cached --quiet -- src; then
echo "has_changes=false" >> "${GITHUB_OUTPUT}"
echo "## Logic4 QA beta" >> "${GITHUB_STEP_SUMMARY}"
echo "No generated source changes were found." >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi

qa_version="$(jq -er '.info.version' logic4-api-qa-V30.json)"
if [[ ! "${qa_version}" =~ ^[0-9A-Za-z._-]+$ ]]; then
echo "Unexpected Logic4 QA version: ${qa_version}" >&2
exit 1
fi

echo "has_changes=true" >> "${GITHUB_OUTPUT}"
echo "qa_version=${qa_version}" >> "${GITHUB_OUTPUT}"

- name: Run quality gates
if: steps.generate.outputs.has_changes == 'true'
run: composer test

- name: Prepare beta candidate
if: steps.generate.outputs.has_changes == 'true'
env:
QA_VERSION: ${{ steps.generate.outputs.qa_version }}
shell: bash
run: |
set -euo pipefail

if ! git diff --quiet -- . ':(exclude)src/**'; then
echo "Generation changed files outside src:" >&2
git diff --name-only -- . ':(exclude)src/**' >&2
exit 1
fi

git add --all src
git diff --cached --check
git diff --cached --binary -- src > beta.patch
sha256sum beta.patch > beta.patch.sha256

jq -n \
--arg base_sha "${GITHUB_SHA}" \
--arg qa_version "${QA_VERSION}" \
--arg source_url "https://qa.api.logic4server.nl/scalar/#latest-version" \
'{base_sha: $base_sha, qa_version: $qa_version, source_url: $source_url}' \
> beta-metadata.json

echo "## Logic4 QA beta candidate" >> "${GITHUB_STEP_SUMMARY}"
echo "QA build: \`${QA_VERSION}\`" >> "${GITHUB_STEP_SUMMARY}"
echo "Base commit: \`${GITHUB_SHA}\`" >> "${GITHUB_STEP_SUMMARY}"
echo '```text' >> "${GITHUB_STEP_SUMMARY}"
git diff --cached --stat -- src >> "${GITHUB_STEP_SUMMARY}"
echo '```' >> "${GITHUB_STEP_SUMMARY}"

- name: Upload beta candidate
if: steps.generate.outputs.has_changes == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: logic4-qa-beta-${{ github.run_id }}
path: |
beta.patch
beta.patch.sha256
beta-metadata.json
if-no-files-found: error
retention-days: 7

release:
if: needs.generate.outputs.has_changes == 'true' && github.event_name == 'workflow_dispatch' && inputs.publish == true && github.ref == 'refs/heads/master'
needs: generate
runs-on: ubuntu-latest
environment: beta-release
permissions:
actions: read
contents: write

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.generate.outputs.base_sha }}
fetch-depth: 0
persist-credentials: false

- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.3'
coverage: pcov

- name: Download beta candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: logic4-qa-beta-${{ github.run_id }}
path: .beta-artifact

- name: Verify beta candidate
working-directory: .beta-artifact
run: sha256sum --check beta.patch.sha256

- name: Apply beta candidate
run: git apply --index .beta-artifact/beta.patch

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction

- name: Run quality gates
run: |
composer validate --strict
composer test
git diff --exit-code
git diff --cached --check

- name: Publish beta release
id: release
env:
DEFAULT_BASE_VERSION: ${{ vars.BETA_BASE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
QA_VERSION: ${{ needs.generate.outputs.qa_version }}
REQUESTED_BASE_VERSION: ${{ inputs.base_version }}
shell: bash
run: |
set -euo pipefail

base_version="${REQUESTED_BASE_VERSION:-${DEFAULT_BASE_VERSION:-}}"
if [[ ! "${base_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Set a valid base_version input or BETA_BASE_VERSION repository variable." >&2
exit 1
fi

git fetch --force --tags origin
latest_beta_tag="$(git tag --list "v${base_version}-beta.*" --sort=-v:refname | head -n 1)"

if [[ -n "${latest_beta_tag}" ]] && git diff --quiet "${latest_beta_tag}" -- src; then
echo "The generated source already matches ${latest_beta_tag}."
echo "published=false" >> "${GITHUB_OUTPUT}"
exit 0
fi

sequence=0
while IFS= read -r existing_tag; do
suffix="${existing_tag##*.}"
if [[ "${suffix}" =~ ^[0-9]+$ ]] && (( suffix > sequence )); then
sequence="${suffix}"
fi
done < <(git tag --list "v${base_version}-beta.*")

tag="v${base_version}-beta.$((sequence + 1))"

git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git commit -m "Generate Logic4 QA client for ${tag}"
git tag --annotate "${tag}" --message "${tag}"

basic_auth="$(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 -w0)"
trap 'git config --local --unset-all http.https://github.com/.extraheader || true' EXIT
git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}"
git push origin "refs/tags/${tag}"
git config --local --unset-all http.https://github.com/.extraheader
trap - EXIT

{
echo "Generated from Logic4 QA build \`${QA_VERSION}\`."
echo
echo "Base commit: \`${GITHUB_SHA}\`"
echo "Documentation: https://qa.api.logic4server.nl/scalar/#latest-version"
} > beta-release-notes.md

gh release create "${tag}" .beta-artifact/beta-metadata.json \
--repo "${GITHUB_REPOSITORY}" \
--verify-tag \
--prerelease \
--title "${tag}" \
--notes-file beta-release-notes.md

echo "published=true" >> "${GITHUB_OUTPUT}"
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
echo "Published ${tag}." >> "${GITHUB_STEP_SUMMARY}"
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ The endpoints can automatically be generated using the `generate` command. This
make generate
```

Production documentation remains the default source. To generate the client from the Logic4 QA documentation instead, run:

```bash
composer generate-beta-endpoints
```

### Beta releases

The `Build Logic4 QA beta` workflow checks the QA documentation daily. Scheduled runs only build, test, and upload a beta candidate when the generated client differs from `master`; they cannot publish a package.

Publishing requires a manual workflow run from `master` with `publish` enabled, a semantic `base_version` such as `4.1.0` or the `BETA_BASE_VERSION` repository variable, and approval through the `beta-release` GitHub environment. A successful run creates a tag such as `v4.1.0-beta.1` and marks its GitHub release as a prerelease.

Install a specific beta with:

```bash
composer require webparking/logic4-client:4.1.0-beta.1
```

## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@
"composer check-coverage"
],
"generate-endpoints": [
"@php generate.php",
"@php generate.php --source=production",
"vendor/bin/rector process --no-diffs --ansi --memory-limit=2G",
"vendor/bin/php-cs-fixer fix"
],
"generate-beta-endpoints": [
"@php generate.php --source=qa --refresh",
"vendor/bin/rector process --no-diffs --ansi --memory-limit=2G",
"vendor/bin/php-cs-fixer fix"
]
Expand Down
25 changes: 21 additions & 4 deletions generate.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,42 @@

use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Webparking\Logic4Client\Generator\ApiDocumentationSource;
use Webparking\Logic4Client\Generator\Generator;

require_once __DIR__.'/vendor/autoload.php';

Debug::enable();
ErrorHandler::register();

$refresh = in_array('--refresh', $argv ?? [], true);
$options = getopt('', ['refresh', 'source:']);

if (false === $options) {
fwrite(\STDERR, "Could not parse generator options.\n");

exit(1);
}

$sourceName = $options['source'] ?? ApiDocumentationSource::Production->value;

if (!is_string($sourceName) || null === ($source = ApiDocumentationSource::tryFrom($sourceName))) {
fwrite(\STDERR, "Source must be either production or qa.\n");

exit(1);
}

$refresh = array_key_exists('refresh', $options);
$generator = new Generator($source);

$versions = array_filter(
Generator::resolveVersions(),
$generator->resolveVersions(),
static fn (string $version): bool => Generator::GENERATED_MAJOR_VERSION === explode('.', $version, 2)[0],
\ARRAY_FILTER_USE_KEY
);

foreach ($versions as $version => $url) {
echo "Generating API namespace $version...\n";
echo "Generating {$source->value} API namespace $version...\n";

$generator = new Generator();
$generator->setRefresh($refresh);
$generator->generate($version, $url);
}
Expand Down
33 changes: 33 additions & 0 deletions generator/ApiDocumentationSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Webparking\Logic4Client\Generator;

use Webmozart\Assert\Assert;

enum ApiDocumentationSource: string
{
case Production = 'production';
case Qa = 'qa';

public function scalarUrl(): string
{
return $this->baseUrl().'scalar/';
}

public function openApiUrl(string $path): string
{
Assert::regex($path, '/\Aopenapi\/[a-z0-9]+(?:\.[a-z0-9]+)*\.json\z/D');

return $this->baseUrl().$path;
}

private function baseUrl(): string
{
return match ($this) {
self::Production => 'https://api.logic4server.nl/',
self::Qa => 'https://qa.api.logic4server.nl/',
};
}
}
Loading