ci: prepare gated qqq-all 4.1 publication #20

Merged
KofTwentyTwo merged 2 commits from feature/GH-10-release-prep into develop 2026-09-26 16:57:37 +00:00
7 changed files with 593 additions and 0 deletions

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
name: Release 4.1.0
on:
workflow_dispatch:
inputs:
release_sha:
description: Full develop commit SHA already tagged 4.1.0
required: true
type: string
smoke_run_id:
description: Successful Smoke workflow run ID for that exact develop SHA
required: true
type: string
permissions:
actions: read
contents: write
packages: write
concurrency:
group: qqq-all-release-4.1.0
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 60
environment: qqq-all-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_SHA: ${{ inputs.release_sha }}
SMOKE_RUN_ID: ${{ inputs.smoke_run_id }}
RELEASE_ENABLED: ${{ vars.QQQ_ALL_RELEASE_ENABLED }}
LICENSE_CLEARED: ${{ vars.QQQ_ALL_RELEASE_LICENSE_APPROVED }}
VERSION: 4.1.0
TAG: 4.1.0
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Check manual release controls and exact tag
shell: bash
run: |
set -euo pipefail
test "$GITHUB_REF" = refs/heads/develop
test "$RELEASE_ENABLED" = true
test "$LICENSE_CLEARED" = true
[[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$SMOKE_RUN_ID" =~ ^[0-9]+$ ]]
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
test "$RELEASE_SHA" = "$GITHUB_SHA"
test "$(git cat-file -t "refs/tags/$TAG")" = tag
test "$(git rev-parse "refs/tags/$TAG^{commit}")" = "$RELEASE_SHA"
- name: Verify upstream GA, exact-commit smoke, Central POMs, and public GHCR
shell: bash
run: |
set -euo pipefail
ga_state="$(gh api repos/QRun-IO/qqq/releases/tags/v4.1.0 \
--jq 'if (.tag_name == "v4.1.0" and .draft == false and .prerelease == false) then "ready" else "blocked" end')"
test "$ga_state" = ready
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SMOKE_RUN_ID" > "$RUNNER_TEMP/smoke-run.json"
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SMOKE_RUN_ID/jobs?per_page=100" > "$RUNNER_TEMP/smoke-jobs.json"
central="https://repo.maven.apache.org/maven2/com/kingsrook/qqq"
for artifact in qqq-all-parent qqq-all-bom; do
curl --fail --silent --show-error \
"$central/$artifact/$VERSION/$artifact-$VERSION.pom" \
--output "$RUNNER_TEMP/$artifact.pom"
done
python3 scripts/release_gate.py --root . --version "$VERSION" --sha "$RELEASE_SHA" \
--smoke-run "$RUNNER_TEMP/smoke-run.json" \
--smoke-jobs "$RUNNER_TEMP/smoke-jobs.json" \
--published-parent "$RUNNER_TEMP/qqq-all-parent.pom" \
--published-bom "$RUNNER_TEMP/qqq-all-bom.pom"
test "$(gh api /orgs/QRun-IO/packages/container/qqq-all --jq .visibility)" = public
gh api --paginate 'orgs/QRun-IO/packages/container/qqq-all/versions?per_page=100' \
--jq '.[] | .metadata.container.tags[]?' > "$RUNNER_TEMP/ghcr-tags.txt"
if grep -Fxq "$VERSION" "$RUNNER_TEMP/ghcr-tags.txt"; then
echo "GHCR tag $VERSION already exists" >&2
exit 1
fi
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Verify release reactor
run: mvn -B -ntp verify
- name: Recheck develop, annotated tag, and release absence before publication
shell: bash
run: |
set -euo pipefail
test "$(gh api "repos/$GITHUB_REPOSITORY/branches/develop" --jq .commit.sha)" = "$RELEASE_SHA"
test "$(git ls-remote origin "refs/tags/$TAG^{}" | cut -f1)" = "$RELEASE_SHA"
if release_check="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$TAG" 2>&1)"; then
echo "GitHub release $TAG already exists" >&2
exit 1
elif [[ "$release_check" != *"HTTP 404"* ]]; then
echo "$release_check" >&2
exit 1
fi
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish versioned image
shell: bash
run: |
set -euo pipefail
image="ghcr.io/qrun-io/qqq-all:$VERSION"
docker build \
--label org.opencontainers.image.source=https://github.com/QRun-IO/qqq-all \
--label "org.opencontainers.image.version=$VERSION" \
--label "org.opencontainers.image.revision=$RELEASE_SHA" \
--tag "$image" .
docker push "$image"
printf 'Published image: `%s`\n' "$image" >> "$GITHUB_STEP_SUMMARY"
- name: Publish GitHub release with Compose asset
shell: bash
run: |
set -euo pipefail
gh release create "$TAG" compose.yaml --repo "$GITHUB_REPOSITORY" \
--verify-tag --title "qqq-all $VERSION" --notes-from-tag
+2
View File
@@ -41,6 +41,8 @@ To stop the full stack, run `docker compose --profile full down`. Add `--volumes
The [develop image workflow](.github/workflows/publish-image.yml) verifies the build and publishes to GHCR only after an organization owner bootstraps the container package and sets its visibility to public. Until then, image publication is skipped and local Compose remains available.
The [4.1.0 release procedure](docs/release.md) describes the separate Maven parent/BOM publication path and the manually gated, exact-commit [release workflow](.github/workflows/release.yml). The workflow will not release the current snapshot build.
## Contributing and license
See [CONTRIBUTING.md](CONTRIBUTING.md), the [Code of Conduct](CODE_OF_CONDUCT.md), and the [security policy](SECURITY.md). Licensed under Apache-2.0; see [LICENSE](LICENSE) and [NOTICE](NOTICE).
+28
View File
@@ -0,0 +1,28 @@
# 4.1.0 release preparation
This procedure prepares a release; this repository currently remains on `4.1.0-SNAPSHOT`. The [manual release workflow](../.github/workflows/release.yml) only publishes the versioned GHCR image and a GitHub release containing `compose.yaml`. It does **not** deploy Maven artifacts or create a tag. Keep the workflow disabled until the owner has cleared the license/header decision and GHCR visibility.
## Prerequisites and Maven artifacts
1. QQQ must publish a non-draft, non-prerelease `v4.1.0` GitHub release with ESB and the Next dashboard included. Release `qbit-build-parent` against the 4.1 BOM, then publish all eight qbits against that parent. This work occurs in their own repositories.
2. In a separate reviewed change to this repository, set root `revision` and the BOM's `qqq.version` to `4.1.0`; pin each qbit to a published non-snapshot version. Run `mvn -B -ntp verify`, the core and full smoke checks, and review the resolved dependencies and effective POMs. Do not use `-Drevision=4.1.0` against a snapshot source tree as a substitute for reviewed pins.
3. Stage **both** `com.kingsrook.qqq:qqq-all-parent:4.1.0` and `com.kingsrook.qqq:qqq-all-bom:4.1.0` using the opt-in `release-central` Maven profile. The profile runs the release-version/dependency checks, signs the POMs with GPG, and uses the Central Publishing Maven plugin with `autoPublish=false`. It is only for the parent and BOM reactor; the app and BOM test JAR are excluded. POM packaging needs no source or Javadoc JARs. Configure the owner's `central` Portal user token in a private Maven `settings.xml` and a trusted GPG key outside the repository. Do not place credentials in this project or CI.
```bash
mvn -B -ntp -pl qqq-all-bom -am -Prelease-central deploy
```
The command uploads a deployment for **manual** Central Portal review; it does not auto-publish. Check the staged parent and BOM POM coordinates, signatures, license/SCM metadata, 4.1 QQQ import, and all eight qbit pins. Publish in the Portal only after approval. The profile fails validation while any current snapshot version remains. `mvn deploy` without the profile is not a release path.
4. Wait until both release POMs resolve anonymously from Maven Central. The release workflow fetches them from `repo.maven.apache.org` and compares their coordinates and BOM pins with its exact source commit. If either artifact is absent or differs, it stops before image publication.
The owner must resolve the license/header alignment noted in the [plan](plan.md) before enabling release. The existing Apache-2.0 metadata is not itself evidence that every dependency and header is cleared. The owner must also bootstrap the `qqq-all` GHCR package and make it **Public** in package settings; first publication defaults private. The workflow checks public visibility and refuses to push a private release image.
## Exact commit and manual dispatch
After the release POMs are public, merge the reviewed version-pin change and the C7 smoke workflow to `develop`. Require a successful **push** run of `Smoke` on the exact intended commit; both `core` and `full` jobs must pass. Review the run's full SHA and run ID in Actions. Create an **annotated** `4.1.0` tag at that exact `develop` commit through the normal signed-tag process. The workflow checks that the local tag is annotated and that the remote tag resolves to the same SHA; it does not verify a tag signature, so the operator must do that separately. Do not move the tag or advance `develop` before dispatch.
Configure a protected `qqq-all-release` GitHub environment with required reviewers and branch restriction to `develop`. After the owner has cleared licensing and the publication preconditions, set environment variables `QQQ_ALL_RELEASE_LICENSE_APPROVED=true` and `QQQ_ALL_RELEASE_ENABLED=true`. Unset them to close the gate. These variables and the environment approval are operational controls, not substitutes for the release review.
Dispatch **Release 4.1.0** on `develop` with `release_sha` (the full 40-character commit SHA) and `smoke_run_id` (the successful push run ID). The job requires that checkout, `develop`, the annotated tag, and the smoke run all refer to that SHA. It checks upstream GA, both public Central POMs, public GHCR visibility, and the absence of a `4.1.0` GHCR tag; runs `mvn -B -ntp verify`; then rechecks the live `develop` and remote tag refs before publication. Only then does it build and push `ghcr.io/qrun-io/qqq-all:4.1.0` and create the GitHub `4.1.0` release with `compose.yaml` attached.
The image push and GitHub release creation are separate API operations, not an atomic transaction. If the release step fails after the image push, inspect the image digest and workflow logs before recovery; never reuse or overwrite the version tag blindly. Verify the public image pull, release asset, and both Maven POMs from an unauthenticated environment after publication. No workflow in this preparation branch publishes or tags anything by itself.
+55
View File
@@ -50,6 +50,61 @@
<maven.compiler.release>21</maven.compiler.release>
</properties>
<profiles>
<!-- Select only the parent and BOM reactor: mvn -pl qqq-all-bom -am -Prelease-central deploy. -->
<profile>
<id>release-central</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>require-ga-revision</id>
<phase>validate</phase>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireProperty>
<property>revision</property>
<regex>^4[.]1[.]0$</regex>
<message>Central release requires a reviewed 4.1.0 source revision.</message>
</requireProperty>
<requireReleaseDeps/>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.8</version>
<executions>
<execution>
<id>sign-release-poms</id>
<phase>verify</phase>
<goals><goal>sign</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.10.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
<autoPublish>false</autoPublish>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<!-- QQQ 4.1.0-SNAPSHOT and other QRun-IO snapshots are published here until 4.1 ships. -->
<repository>
+37
View File
@@ -38,6 +38,43 @@
<rabbitmq-jms.version>3.9.0</rabbitmq-jms.version>
</properties>
<profiles>
<profile>
<id>release-central</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>require-release-bom-pins</id>
<phase>validate</phase>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireProperty>
<property>qqq.version</property>
<regex>^4[.]1[.]0$</regex>
</requireProperty>
<requireProperty><property>qbit-quick-search.version</property><regex>^(?!.*SNAPSHOT).+$</regex></requireProperty>
<requireProperty><property>qbit-user-role-permissions.version</property><regex>^(?!.*SNAPSHOT).+$</regex></requireProperty>
<requireProperty><property>qbit-customizable-table-views.version</property><regex>^(?!.*SNAPSHOT).+$</regex></requireProperty>
<requireProperty><property>qbit-standard-process-trace.version</property><regex>^(?!.*SNAPSHOT).+$</regex></requireProperty>
<requireProperty><property>qbit-webhooks.version</property><regex>^(?!.*SNAPSHOT).+$</regex></requireProperty>
<requireProperty><property>qbit-workflows.version</property><regex>^(?!.*SNAPSHOT).+$</regex></requireProperty>
<requireProperty><property>qbit-geo-data.version</property><regex>^(?!.*SNAPSHOT).+$</regex></requireProperty>
<requireProperty><property>qbit-sftp-data-integration.version</property><regex>^(?!.*SNAPSHOT).+$</regex></requireProperty>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencyManagement>
<dependencies>
<!-- QQQ modules, qqq-esb, and qqq-frontend-next. -->
+183
View File
@@ -0,0 +1,183 @@
"""Fail-closed checks for the manual 4.1.0 release workflow."""
import argparse
import json
import re
import sys
from pathlib import Path
from xml.etree import ElementTree
QBITS = (
"quick-search", "user-role-permissions", "customizable-table-views",
"standard-process-trace", "webhooks", "workflows", "geo-data",
"sftp-data-integration",
)
class GateError(ValueError):
pass
def properties(path):
root = ElementTree.parse(path).getroot()
namespace = xml_namespace(root)
values = root.find(f"{namespace}properties")
if values is None:
raise GateError(f"Missing Maven properties in {path}")
return {child.tag.removeprefix(namespace): (child.text or "").strip() for child in values}
def xml_namespace(root):
return root.tag.partition("}")[0] + "}" if root.tag.startswith("{") else ""
def coordinates(path):
root = ElementTree.parse(path).getroot()
namespace = xml_namespace(root)
parent = root.find(f"{namespace}parent")
def value(name):
direct = root.findtext(f"{namespace}{name}")
inherited = parent.findtext(f"{namespace}{name}") if parent is not None else None
return direct or inherited
return value("groupId"), value("artifactId"), value("version")
def managed_dependencies(path):
root = ElementTree.parse(path).getroot()
namespace = xml_namespace(root)
values = properties(path)
if root.findall(f"{namespace}profiles/{namespace}profile/{namespace}dependencyManagement"):
raise GateError(f"Profile-managed dependency override in {path}")
dependencies = root.find(f"{namespace}dependencyManagement/{namespace}dependencies")
if dependencies is None:
raise GateError(f"Missing dependencyManagement in {path}")
def field(dependency, name, default=""):
return (dependency.findtext(f"{namespace}{name}") or default).strip()
managed = []
for dependency in dependencies.findall(f"{namespace}dependency"):
version = field(dependency, "version")
reference = re.fullmatch(r"\$\{([^}]+)\}", version)
if reference:
version = values.get(reference.group(1), "")
if not version or "${" in version or "SNAPSHOT" in version.upper():
raise GateError(f"Unreleased managed dependency in {path}: "
f"{field(dependency, 'artifactId')}")
managed.append((field(dependency, "groupId"), field(dependency, "artifactId"),
version, field(dependency, "type", "jar"),
field(dependency, "scope", "compile"),
field(dependency, "classifier")))
if not managed:
raise GateError(f"No managed dependencies in {path}")
return managed
def validate_managed_bom(path, version):
managed = managed_dependencies(path)
entries = {(group, artifact): (pin, kind, scope) for group, artifact, pin, kind, scope, _ in managed}
if len(entries) != len(managed):
raise GateError(f"Duplicate managed dependency in {path}")
if entries.get(("com.kingsrook.qqq", "qqq-bom-pom")) != (version, "pom", "import"):
raise GateError(f"QQQ BOM import differs from {version} in {path}")
values = properties(path)
for name in QBITS:
artifact = f"qbit-{name}"
if entries.get(("com.kingsrook.qbits", artifact)) != (
values.get(f"{artifact}.version"), "jar", "compile"):
raise GateError(f"Managed {artifact} differs from its release pin in {path}")
return managed
def validate_published_poms(parent_path, bom_path, version, local_bom):
expected = (
(parent_path, "qqq-all-parent"),
(bom_path, "qqq-all-bom"),
)
for path, artifact in expected:
if coordinates(path) != ("com.kingsrook.qqq", artifact, version):
raise GateError(f"Published {artifact} POM is not the {version} release")
published = properties(bom_path)
if published.get("qqq.version") != version:
raise GateError("Published BOM does not import QQQ 4.1.0")
for name in QBITS:
key = f"qbit-{name}.version"
value = published.get(key, "")
if not value or "SNAPSHOT" in value.upper():
raise GateError(f"Published BOM {key} is not a release")
local = properties(local_bom)
for key in ("qqq.version", *(f"qbit-{name}.version" for name in QBITS)):
if published.get(key) != local.get(key):
raise GateError(f"Published BOM {key} differs from release source")
if validate_managed_bom(bom_path, version) != validate_managed_bom(local_bom, version):
raise GateError("Published BOM dependencyManagement differs from release source")
def validate_poms(root, version):
if version != "4.1.0":
raise GateError("This release workflow is limited to 4.1.0")
parent = properties(root / "pom.xml")
bom = properties(root / "qqq-all-bom/pom.xml")
if parent.get("revision") != version or bom.get("qqq.version") != version:
raise GateError("Parent revision and QQQ BOM pin must both equal 4.1.0")
for name in QBITS:
key = f"qbit-{name}.version"
value = bom.get(key, "")
if not value or "SNAPSHOT" in value.upper():
raise GateError(f"{key} must be a published non-snapshot version")
for key, value in bom.items():
if key.endswith(".version") and "SNAPSHOT" in value.upper():
raise GateError(f"{key} still points to a snapshot")
validate_managed_bom(root / "qqq-all-bom/pom.xml", version)
def validate_smoke(run, jobs, sha):
if not re.fullmatch(r"[0-9a-f]{40}", sha):
raise GateError("Release SHA must be a full lowercase Git commit ID")
expected = {
"name": "Smoke", "path": ".github/workflows/smoke.yml",
"head_sha": sha, "head_branch": "develop", "event": "push",
"conclusion": "success",
}
for key, value in expected.items():
if run.get(key) != value:
raise GateError(f"Smoke run {key} must equal {value}")
observed = {"core": False, "full": False}
for job in jobs.get("jobs", []):
name = job.get("name")
if name in observed:
if job.get("conclusion") != "success":
raise GateError(f"Smoke job {name} did not pass")
observed[name] = True
if not all(observed.values()):
raise GateError("Smoke run must include successful core and full jobs")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--version", required=True)
parser.add_argument("--sha", required=True)
parser.add_argument("--smoke-run", type=Path, required=True)
parser.add_argument("--smoke-jobs", type=Path, required=True)
parser.add_argument("--published-parent", type=Path, required=True)
parser.add_argument("--published-bom", type=Path, required=True)
args = parser.parse_args()
try:
validate_poms(args.root, args.version)
validate_smoke(json.loads(args.smoke_run.read_text()),
json.loads(args.smoke_jobs.read_text()), args.sha)
validate_published_poms(args.published_parent, args.published_bom, args.version,
args.root / "qqq-all-bom/pom.xml")
except (GateError, OSError, ElementTree.ParseError, json.JSONDecodeError) as error:
print(f"Release gate failed: {error}", file=sys.stderr)
return 1
print("Local POM and exact-commit smoke gates passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+164
View File
@@ -0,0 +1,164 @@
"""Release gate checks use fixtures so the workflow can fail closed before publication."""
import tempfile
import unittest
from pathlib import Path
from release_gate import GateError, validate_poms, validate_published_poms, validate_smoke
RELEASE_SHA = "a" * 40
QBITS = (
"quick-search", "user-role-permissions", "customizable-table-views",
"standard-process-trace", "webhooks", "workflows", "geo-data",
"sftp-data-integration",
)
def management(qqq_version="${qqq.version}", qbit_version=None):
qbit_version = qbit_version or (lambda name: f"${{qbit-{name}.version}}")
dependencies = (
"<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId>"
f"<version>{qqq_version}</version><type>pom</type><scope>import</scope></dependency>"
)
dependencies += "".join(
"<dependency><groupId>com.kingsrook.qbits</groupId>"
f"<artifactId>qbit-{name}</artifactId><version>{qbit_version(name)}</version></dependency>"
for name in QBITS
)
return f"<dependencyManagement><dependencies>{dependencies}</dependencies></dependencyManagement>"
class ReleaseGateTest(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
(self.root / "qqq-all-bom").mkdir()
self.write_poms("4.1.0", "4.1.0", "1.2.3")
def write_poms(self, revision, qqq_version, qbit_version):
(self.root / "pom.xml").write_text(
f"<project><properties><revision>{revision}</revision></properties></project>"
)
(self.root / "qqq-all-bom/pom.xml").write_text(
"<project><properties>"
f"<qqq.version>{qqq_version}</qqq.version>"
+ "".join(f"<qbit-{name}.version>{qbit_version}</qbit-{name}.version>" for name in QBITS)
+ "</properties>" + management() + "</project>"
)
def published_poms(self, resolved=False):
parent = self.root / "published-parent.pom"
bom = self.root / "published-bom.pom"
parent.write_text("<project xmlns='http://maven.apache.org/POM/4.0.0'>"
"<groupId>com.kingsrook.qqq</groupId>"
"<artifactId>qqq-all-parent</artifactId><version>4.1.0</version></project>")
managed = (management("4.1.0", lambda name: "1.2.3") if resolved else management())
bom.write_text("<project xmlns='http://maven.apache.org/POM/4.0.0'><parent>"
"<groupId>com.kingsrook.qqq</groupId>"
"<artifactId>qqq-all-parent</artifactId><version>4.1.0</version></parent>"
"<artifactId>qqq-all-bom</artifactId><properties><qqq.version>4.1.0</qqq.version>"
+ "".join(f"<qbit-{name}.version>1.2.3</qbit-{name}.version>" for name in QBITS)
+ "</properties>" + managed + "</project>")
return parent, bom
def test_release_poms_require_ga_versions(self):
validate_poms(self.root, "4.1.0")
for revision, qqq_version, qbit_version in (
("4.1.0-SNAPSHOT", "4.1.0", "1.2.3"),
("4.1.0", "4.1.0-SNAPSHOT", "1.2.3"),
("4.1.0", "4.1.0", "1.2.3-SNAPSHOT"),
):
with self.subTest(revision=revision, qqq=qqq_version, qbit=qbit_version):
self.write_poms(revision, qqq_version, qbit_version)
with self.assertRaises(GateError):
validate_poms(self.root, "4.1.0")
self.write_poms("4.1.0", "4.1.0", "1.2.3")
source_bom = self.root / "qqq-all-bom/pom.xml"
source_bom.write_text(source_bom.read_text().replace(
"<artifactId>qbit-webhooks</artifactId>",
"<artifactId>qbit-other</artifactId>"))
with self.assertRaises(GateError):
validate_poms(self.root, "4.1.0")
def test_smoke_run_must_match_release_sha_and_both_jobs(self):
run = {
"name": "Smoke", "path": ".github/workflows/smoke.yml",
"head_sha": RELEASE_SHA, "head_branch": "develop", "event": "push",
"conclusion": "success",
}
jobs = {"jobs": [
{"name": "core", "conclusion": "success"},
{"name": "full", "conclusion": "success"},
]}
validate_smoke(run, jobs, RELEASE_SHA)
for changed_run, changed_jobs in (
({**run, "head_sha": "b" * 40}, jobs),
({**run, "conclusion": "failure"}, jobs),
({**run, "path": ".github/workflows/other.yml"}, jobs),
(run, {"jobs": [jobs["jobs"][0]]}),
(run, {"jobs": [jobs["jobs"][0], {"name": "full", "conclusion": "skipped"}]}),
):
with self.subTest(run=changed_run, jobs=changed_jobs):
with self.assertRaises(GateError):
validate_smoke(changed_run, changed_jobs, RELEASE_SHA)
def test_published_parent_and_bom_must_be_release_poms(self):
parent, bom = self.published_poms()
validate_published_poms(parent, bom, "4.1.0", self.root / "qqq-all-bom/pom.xml")
self.published_poms(resolved=True)
validate_published_poms(parent, bom, "4.1.0", self.root / "qqq-all-bom/pom.xml")
parent.write_text(parent.read_text().replace("qqq-all-parent", "wrong-parent"))
with self.assertRaises(GateError):
validate_published_poms(parent, bom, "4.1.0", self.root / "qqq-all-bom/pom.xml")
self.published_poms(resolved=True)
bom.write_text(bom.read_text().replace("4.1.0", "4.1.0-SNAPSHOT"))
with self.assertRaises(GateError):
validate_published_poms(parent, bom, "4.1.0", self.root / "qqq-all-bom/pom.xml")
def test_published_bom_rejects_managed_dependency_overrides(self):
parent, bom = self.published_poms(resolved=True)
original = bom.read_text()
mutations = (
original.replace("<artifactId>qqq-bom-pom</artifactId>",
"<artifactId>qqq-other-bom</artifactId>"),
original.replace("<type>pom</type><scope>import</scope>",
"<type>pom</type><scope>compile</scope>"),
original.replace("<version>4.1.0</version><type>pom</type><scope>import</scope>",
"<version>4.1.0-SNAPSHOT</version><type>pom</type><scope>import</scope>"),
original.replace("<artifactId>qbit-webhooks</artifactId><version>1.2.3</version>",
"<artifactId>qbit-webhooks</artifactId><version>1.2.3-SNAPSHOT</version>"),
original.replace("<artifactId>qbit-webhooks</artifactId><version>1.2.3</version>",
"<artifactId>qbit-webhooks</artifactId><version>1.2.4</version>"),
original.replace("<dependency><groupId>com.kingsrook.qbits</groupId>"
"<artifactId>qbit-webhooks</artifactId><version>1.2.3</version>"
"</dependency>", ""),
original.replace("</dependencies></dependencyManagement>",
"<dependency><groupId>com.kingsrook.qbits</groupId>"
"<artifactId>qbit-webhooks</artifactId><version>1.2.4</version>"
"</dependency></dependencies></dependencyManagement>"),
original.replace("</project>", "<profiles><profile><id>override</id>"
"<activation><activeByDefault>true</activeByDefault></activation>"
"<dependencyManagement><dependencies><dependency>"
"<groupId>com.kingsrook.qbits</groupId>"
"<artifactId>qbit-webhooks</artifactId>"
"<version>1.2.3-SNAPSHOT</version>"
"</dependency></dependencies></dependencyManagement>"
"</profile></profiles></project>"),
)
for index, mutation in enumerate(mutations):
with self.subTest(mutation=index):
self.assertNotEqual(original, mutation)
bom.write_text(mutation)
with self.assertRaises(GateError):
validate_published_poms(parent, bom, "4.1.0", self.root / "qqq-all-bom/pom.xml")
bom.write_text(original.replace(
"<qbit-webhooks.version>1.2.3</qbit-webhooks.version>",
"<qbit-webhooks.version>1.2.4</qbit-webhooks.version>"))
with self.assertRaises(GateError):
validate_published_poms(parent, bom, "4.1.0", self.root / "qqq-all-bom/pom.xml")
if __name__ == "__main__":
unittest.main()