diff --git a/.github/actions/setup-integration/action.yml b/.github/actions/setup-integration/action.yml new file mode 100644 index 000000000..57f076a90 --- /dev/null +++ b/.github/actions/setup-integration/action.yml @@ -0,0 +1,49 @@ +--- +name: Prepare integration dependencies +description: Restore Workstation and cache downloads for an exact Policyfile dependency snapshot. +inputs: + dependencies: + description: Resolved Git dependency revisions. + required: true + dependency-key: + description: Hash of the Policyfile and resolved Git dependencies. + required: true + workstation-version: + description: Exact Workstation version selected by the prepare job. + default: latest + workstation-installed: + description: Skip restoring Workstation when prepare has already installed it. + default: 'false' +runs: + using: composite + steps: + - name: Restore Cinc Workstation + id: workstation + if: inputs.workstation-installed != 'true' + uses: sous-chefs/.github/.github/actions/install-workstation@codex/cache-cinc-workstation + with: + version: ${{ inputs.workstation-version }} + - name: Configure dependency cache and pin Git revisions + shell: bash + env: + DEPENDENCIES: ${{ inputs.dependencies }} + run: | + echo "CHEF_WORKSTATION_HOME=$RUNNER_TEMP/chef-workstation" >> "$GITHUB_ENV" + git config --global maintenance.auto false + git config --global gc.auto 0 + python3 .github/ci/integration.py pin + - name: Restore cookbook downloads + id: cache + uses: actions/cache/restore@v5 + with: + path: ${{ runner.temp }}/chef-workstation/cache + key: policyfile-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.workstation.outputs.version || inputs.workstation-version }}-${{ inputs.dependency-key }} + - name: Resolve cookbooks for this checkout + shell: bash + run: /opt/cinc-workstation/bin/chef-cli install Policyfile.rb + - name: Save cookbook downloads + if: steps.cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ${{ runner.temp }}/chef-workstation/cache + key: policyfile-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.workstation.outputs.version || inputs.workstation-version }}-${{ inputs.dependency-key }} diff --git a/.github/ci/.gitignore b/.github/ci/.gitignore new file mode 100644 index 000000000..c18dd8d83 --- /dev/null +++ b/.github/ci/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/.github/ci/README.md b/.github/ci/README.md new file mode 100644 index 000000000..8895830ec --- /dev/null +++ b/.github/ci/README.md @@ -0,0 +1,64 @@ +# Integration performance + +Each OS job runs its suites on one GitHub-hosted runner. Workstation, Kitchen, +kitchen-dokken and downloaded base images are shared within that job. Each suite +still runs `kitchen test --destroy=always` with two converges and a fresh container. + +The prepare job selects one Workstation version and exact cookbook Git revisions, +then restores or populates the cookbook download cache before the matrix starts. +Every matrix job regenerates its Policyfile lock for its own checkout. Neither +Kitchen state nor converged containers are cached. + +Grouped jobs have a 30-minute deadline to prevent a stalled suite holding a +runner for hours. + +CI sets `KITCHEN_REUSE_IMAGES=true`: Dokken pulls an image when it is missing, +then uses that local image for later suites. Local runs keep Dokken's usual +pull-on-each-create behaviour unless explicitly given that variable. Standard +GitHub-hosted VMs are fresh for each job; this does not retain a VM across runs. + +## Docker image delivery benchmark + +[Run on 20 October 2026](https://github.com/sous-chefs/apache2/actions/runs/39509952839) +compared three fresh runners per method and OS. Every pair included the Cinc +image and the OS image. Both methods used identical digests and checked image IDs. + +| OS | Pull median (range) | Restore and load median (range) | +| --- | ---: | ---: | +| AlmaLinux 9 | 11.05s (10.69–12.01s) | 13.84s (13.31–19.72s) | +| openSUSE Leap 15 | 15.96s (14.97–20.03s) | 19.89s (18.87–20.61s) | +| Ubuntu 24.04 | 14.35s (13.56–17.61s) | 21.11s (15.77–24.54s) | + +Restore timing includes Actions cache download, decompression and `docker load`. +Pull timing includes fetching the digest and tagging it for Dokken. Both exclude +checkout, manifest download, runner setup and queue time. Cache creation is an +additional cost reported in the seed jobs. Direct pulls won all three medians, +so normal integration jobs do not restore Docker image archives. + +The separate image benchmark only runs on manual dispatch or changes to its own +workflow/script. Its artifacts retain the individual measurements for seven days. +The results above describe these images and runners; they are not a universal +claim that image caching is slower. + +## Suite grouping comparison + +The [initial comparison](https://github.com/sous-chefs/apache2/actions/runs/39509953258) +used the same commit, cookbook revisions, runner type and eight-job limit for +both layouts. Nine complete matched OS groups covered 78 suites: + +| Layout | Active job time | +| --- | ---: | +| Individual suite jobs | 7,359s (122.7 minutes) | +| Suites grouped by OS | 4,931s (82.2 minutes) | + +Grouping used 33.0% fewer active runner minutes for that subset. This excludes +queue time and does not measure end-to-end completion time. The complete matrix +comparison did not finish: two individual AlmaLinux jobs were still in Dokken +after almost 30 minutes. At the recorded snapshot, 15 grouped OS jobs and 99 +individual jobs had passed. Unfinished OS comparisons are excluded from the +figures above, so the result must not be presented as a full-matrix saving. + +The temporary individual baseline jobs have been removed. The normal grouped +workflow retains all 148 original suite/platform combinations. The additional +image-reuse and cache-priming changes were applied after this comparison; their +benefit is not included in the measured saving. diff --git a/.github/ci/docker_images.py b/.github/ci/docker_images.py new file mode 100644 index 000000000..9d489d62f --- /dev/null +++ b/.github/ci/docker_images.py @@ -0,0 +1,99 @@ +"""Compare registry pulls with restored Docker archives on fresh runners.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import statistics +import subprocess +import time + + +def docker(*args): + return subprocess.check_output(["docker", *args], text=True).strip() + + +def inspect(tag): + return json.loads(docker("image", "inspect", tag))[0] + + +def validate(images): + for image in images: + if inspect(image["tag"])["Id"] != image["id"]: + raise ValueError("Wrong image loaded: " + image["tag"]) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=["seed", "start", "pull", "load", "report"]) + args = parser.parse_args() + root = Path(os.environ.get("RUNNER_TEMP", "/tmp")) + archive = root / "dokken-images.tar" + manifest = root / "images.json" + clock = root / "image-start.txt" + if args.action == "seed": + tags = ["cincproject/cinc:latest", os.environ["PLATFORM_IMAGE"] + ":latest"] + images = [] + started = time.monotonic() + for tag in tags: + docker("pull", "--platform", "linux/amd64", tag) + row = inspect(tag) + images.append(dict(tag=tag, ref=row["RepoDigests"][0], id=row["Id"])) + pull_seconds = time.monotonic() - started + started = time.monotonic() + docker("save", "--output", str(archive), *tags) + manifest.write_text(json.dumps(images)) + key = "dokken-images-v1-Linux-X64-" + hashlib.sha256(json.dumps(images, sort_keys=True).encode()).hexdigest() + with open(os.environ["GITHUB_OUTPUT"], "a") as output: + output.write("key=" + key + "\n") + with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as output: + output.write(f"Seed {tags[1]}: pull {pull_seconds:.2f}s; docker save {time.monotonic()-started:.2f}s; archive {archive.stat().st_size/1024**2:.1f} MiB.\n") + elif args.action == "start": + # Called before actions/cache, so restore includes download and extraction. + images = json.loads(manifest.read_text()) + for image in images: + result = subprocess.run(["docker", "image", "inspect", image["tag"]], capture_output=True) + if result.returncode == 0: + raise ValueError("Benchmark runner already contains " + image["tag"]) + clock.write_text(str(time.monotonic())) + elif args.action in {"pull", "load"}: + images = json.loads(manifest.read_text()) + if args.action == "pull": + for image in images: + docker("pull", "--platform", "linux/amd64", image["ref"]) + docker("tag", image["ref"], image["tag"]) + else: + docker("load", "--input", str(archive)) + validate(images) + result = dict(os=os.environ["PLATFORM"], mode=args.action, + trial=int(os.environ["TRIAL"]), + seconds=time.monotonic()-float(clock.read_text()), + images=images) + Path(f"result-{result['os']}-{result['mode']}-{result['trial']}.json").write_text(json.dumps(result)) + with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as output: + output.write(f"{result['os']} {result['mode']} trial {result['trial']}: **{result['seconds']:.2f}s**, both image IDs verified.\n") + else: + rows = [json.loads(p.read_text()) for p in Path("results").glob("result-*.json")] + if len(rows) != 18: + raise ValueError(f"Expected 18 measurements, got {len(rows)}") + lines = ["| OS | Registry pull median (range) | Cache restore + load median (range) |", "| --- | ---: | ---: |"] + for platform in sorted({r['os'] for r in rows}): + columns = [] + snapshots = {json.dumps(r['images'], sort_keys=True) for r in rows if r['os']==platform} + if len(snapshots) != 1: + raise ValueError("Benchmark image snapshots differ") + for mode in ("pull", "load"): + values = [r['seconds'] for r in rows if r['os']==platform and r['mode']==mode] + if len(values) != 3: + raise ValueError("Missing repeats") + columns.append(f"{statistics.median(values):.2f}s ({min(values):.2f}–{max(values):.2f}s)") + lines.append("| " + " | ".join([platform, *columns]) + " |") + lines.append("\nCache timing includes Actions download/decompression and docker load. Pull timing includes digest pulls and tagging. Checkout, manifest download and runner queue/setup are excluded from both. Cache creation is reported separately by seed jobs; it is not free. No containers or runner state are reused.") + report = "\n".join(lines) + "\n" + print(report) + with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as output: + output.write(report) + + +if __name__ == "__main__": + main() diff --git a/.github/ci/integration.py b/.github/ci/integration.py new file mode 100644 index 000000000..664fa2e22 --- /dev/null +++ b/.github/ci/integration.py @@ -0,0 +1,109 @@ +"""Plan isolated Kitchen suites, classify changes and pin dependency snapshots.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import sys + +# Preserve the existing CI coverage during the grouping trial. +SUITES = {"basic-site", "default", "mod-auth-cas", "php", "module-template", + "mod-wsgi", "ports", "ssl", "install-override"} +DEPENDENCY = re.compile(r"cookbook '([^']+)', git: '([^']+)', branch: '([^']+)'") + + +def documentation_only(paths): + return bool(paths) and all( + p.lower().endswith(".md") or p.startswith(("docs/", "documentation/")) + or p in {"LICENSE", "CODE_OF_CONDUCT", "CONTRIBUTING", "TESTING"} + for p in paths) + + +def plan(instances): + groups = {} + for row in instances: + name = row["instance"] + suite = next((s for s in SUITES if name.startswith(s + "-")), None) + if suite: + platform = name[len(suite) + 1:] + groups.setdefault(platform, []).append(name) + grouped = [{"os": p, "instances": sorted(names)} for p, names in sorted(groups.items())] + individual = [{"os": g["os"], "instances": [n]} for g in grouped for n in g["instances"]] + return grouped, individual + + +def pin(source, manifest): + def replacement(match): + name, url, branch = match.groups() + entry = manifest[name] + if entry["url"] != url or entry["branch"] != branch: + raise ValueError("Dependency manifest does not match " + name) + revision = entry["revision"] + if not re.fullmatch(r"[0-9a-f]{40}", revision): + raise ValueError("Invalid Git revision for " + name) + return f"cookbook '{name}', git: '{url}', ref: '{revision}'" + if len(DEPENDENCY.findall(source)) != len(manifest): + raise ValueError("Dependency manifest is incomplete") + return DEPENDENCY.sub(replacement, source) + + +def run_instances(instances): + failures = [] + env = dict(os.environ, MULTIPLE_CONVERGE="2") + for instance in instances: + print("::group::Kitchen " + instance, flush=True) + result = subprocess.run(["kitchen", "test", "^" + re.escape(instance) + "$", + "--destroy=always"], env=env, check=False) + print("::endgroup::", flush=True) + if result.returncode: + failures.append(instance) + if failures: + print("::error::Failed instances: " + ", ".join(failures)) + return bool(failures) + + +def output(values): + with open(os.environ["GITHUB_OUTPUT"], "a") as stream: + for key, value in values.items(): + stream.write(key + "=" + (value if isinstance(value, str) else json.dumps(value, separators=(",", ":"))) + "\n") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=["changes", "prepare", "pin", "run"]) + args = parser.parse_args() + if args.action == "changes": + base, head = os.environ.get("BASE_SHA", ""), os.environ.get("HEAD_SHA", "HEAD") + paths = [] + if base and base.strip("0"): + result = subprocess.run(["git", "diff", "--name-only", "--no-renames", "-z", base, head], + text=True, capture_output=True, check=False) + if result.returncode == 0: + paths = [p for p in result.stdout.split("\0") if p] + output({"integration": "false" if documentation_only(paths) else "true"}) + elif args.action == "prepare": + instances = json.loads(subprocess.check_output(["kitchen", "list", "--json"], text=True)) + grouped, individual = plan(instances) + if len(individual) != 148 or len(grouped) != 17: + raise ValueError("Kitchen coverage changed; review the CI plan before continuing") + source = Path("Policyfile.rb").read_text() + manifest = {} + for name, url, branch in DEPENDENCY.findall(source): + revision = subprocess.check_output(["git", "ls-remote", "--exit-code", url, "refs/heads/" + branch], text=True).split()[0] + manifest[name] = dict(url=url, branch=branch, revision=revision) + digest = hashlib.sha256((source + json.dumps(manifest, sort_keys=True)).encode()).hexdigest() + output(dict(grouped={"include": grouped}, + dependencies=manifest, dependency_key=digest)) + print(f"Planned {len(individual)} suites across {len(grouped)} fresh runners") + elif args.action == "pin": + path = Path("Policyfile.rb") + path.write_text(pin(path.read_text(), json.loads(os.environ["DEPENDENCIES"]))) + else: + return run_instances(json.loads(os.environ["KITCHEN_INSTANCES"])) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/ci/test_integration.py b/.github/ci/test_integration.py new file mode 100644 index 000000000..714d58ec2 --- /dev/null +++ b/.github/ci/test_integration.py @@ -0,0 +1,40 @@ +import io +import unittest +from unittest.mock import patch +import integration + + +class IntegrationTest(unittest.TestCase): + def test_docs_filter_is_conservative(self): + self.assertTrue(integration.documentation_only(["README.md", "documentation/example.md"])) + for paths in ([], ["resources/module.rb"], ["README.md", "Policyfile.rb"], [".github/workflows/ci.yml"]): + self.assertFalse(integration.documentation_only(paths)) + + def test_grouping_preserves_exact_instance_set_and_exclusions(self): + names = ["default-ubuntu-2404", "php-ubuntu-2404", "default-almalinux-10", "custom-template-ubuntu-2404"] + grouped, individual = integration.plan([{"instance": n} for n in names]) + self.assertEqual(len(grouped), 2) + self.assertEqual(sorted(n for g in grouped for n in g["instances"]), sorted(r["instances"][0] for r in individual)) + self.assertNotIn("custom-template-ubuntu-2404", [r["instances"][0] for r in individual]) + self.assertNotIn("mod-auth-cas-almalinux-10", [r["instances"][0] for r in individual]) + + def test_pin_requires_matching_snapshot(self): + source = "cookbook 'apt', git: 'https://example/apt.git', branch: 'main'\n" + manifest = {"apt": dict(url="https://example/apt.git", branch="main", revision="a" * 40)} + self.assertIn("ref: '" + "a" * 40 + "'", integration.pin(source, manifest)) + with self.assertRaises(ValueError): + integration.pin(source, {}) + + @patch("integration.subprocess.run") + @patch("sys.stdout", new_callable=io.StringIO) + def test_each_suite_gets_its_own_lifecycle_even_after_failure(self, stdout, run): + run.return_value.returncode = 1 + self.assertTrue(integration.run_instances(["default-ubuntu-2404", "php-ubuntu-2404"])) + self.assertEqual(run.call_count, 2) + for call in run.call_args_list: + self.assertEqual(call.args[0][-1], "--destroy=always") + self.assertEqual(call.kwargs["env"]["MULTIPLE_CONVERGE"], "2") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/benchmark-dokken.yml b/.github/workflows/benchmark-dokken.yml new file mode 100644 index 000000000..96020591d --- /dev/null +++ b/.github/workflows/benchmark-dokken.yml @@ -0,0 +1,100 @@ +--- +name: Benchmark Dokken image delivery + +"on": + workflow_dispatch: + pull_request: + paths: + - .github/workflows/benchmark-dokken.yml + - .github/ci/docker_images.py + +permissions: + contents: read + +concurrency: + group: dokken-benchmark-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + seed: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, almalinux-9, opensuse-leap-15] + steps: + - uses: actions/checkout@v7 + - name: Pull exact images and export archive + id: seed + env: + PLATFORM_IMAGE: dokken/${{ matrix.os }} + run: python3 .github/ci/docker_images.py seed + - name: Save immutable image archive + uses: actions/cache/save@v5 + with: + path: ${{ runner.temp }}/dokken-images.tar + key: ${{ steps.seed.outputs.key }} + - name: Record cache key + env: + CACHE_KEY: ${{ steps.seed.outputs.key }} + run: echo "$CACHE_KEY" > "$RUNNER_TEMP/cache-key.txt" + - uses: actions/upload-artifact@v4 + with: + name: image-manifest-${{ matrix.os }} + path: | + ${{ runner.temp }}/images.json + ${{ runner.temp }}/cache-key.txt + retention-days: 7 + + measure: + needs: seed + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + max-parallel: 6 + matrix: + os: [ubuntu-24.04, almalinux-9, opensuse-leap-15] + mode: [pull, load] + trial: [1, 2, 3] + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v4 + with: + name: image-manifest-${{ matrix.os }} + path: ${{ runner.temp }} + - name: Read cache key + id: key + run: echo "key=$(cat "$RUNNER_TEMP/cache-key.txt")" >> "$GITHUB_OUTPUT" + - name: Verify cold runner and start timer + run: python3 .github/ci/docker_images.py start + - name: Restore image archive + if: matrix.mode == 'load' + uses: actions/cache/restore@v5 + with: + path: ${{ runner.temp }}/dokken-images.tar + key: ${{ steps.key.outputs.key }} + fail-on-cache-miss: true + - name: Deliver images and verify identical contents + env: + PLATFORM: ${{ matrix.os }} + TRIAL: ${{ matrix.trial }} + DELIVERY: ${{ matrix.mode }} + run: python3 .github/ci/docker_images.py "$DELIVERY" + - uses: actions/upload-artifact@v4 + with: + name: docker-image-result-${{ matrix.os }}-${{ matrix.mode }}-${{ matrix.trial }} + path: result-*.json + retention-days: 7 + + report: + needs: measure + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v4 + with: + pattern: docker-image-result-* + path: results + merge-multiple: true + - name: Compare repeated measurements + run: python3 .github/ci/docker_images.py report diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e48a65bc4..a8f52b041 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,30 @@ name: ci push: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: + changes: + runs-on: ubuntu-24.04 + outputs: + integration: ${{ steps.changes.outputs.integration }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Test CI planning + run: python3 -m unittest discover -s .github/ci -p 'test_*.py' + - name: Classify changes + id: changes + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 .github/ci/integration.py changes + lint-unit: - uses: sous-chefs/.github/.github/workflows/lint-unit.yml@9.0.1 + uses: sous-chefs/.github/.github/workflows/lint-unit.yml@codex/cache-cinc-workstation permissions: actions: write checks: write @@ -17,86 +38,82 @@ jobs: issues: write secrets: inherit + prepare: + needs: [changes, lint-unit] + if: needs.changes.outputs.integration == 'true' + runs-on: ubuntu-24.04 + outputs: + grouped: ${{ steps.plan.outputs.grouped }} + dependencies: ${{ steps.plan.outputs.dependencies }} + dependency-key: ${{ steps.plan.outputs.dependency_key }} + workstation-version: ${{ steps.workstation.outputs.version }} + env: + KITCHEN_LOCAL_YAML: kitchen.dokken.yml + KITCHEN_PRODUCT_NAME: cinc + steps: + - uses: actions/checkout@v7 + - name: Restore Cinc Workstation + id: workstation + uses: sous-chefs/.github/.github/actions/install-workstation@codex/cache-cinc-workstation + - name: Plan identical suite coverage and dependency revisions + id: plan + run: python3 .github/ci/integration.py prepare + - name: Warm cookbook downloads before starting the matrix + uses: ./.github/actions/setup-integration + with: + dependencies: ${{ steps.plan.outputs.dependencies }} + dependency-key: ${{ steps.plan.outputs.dependency_key }} + workstation-version: ${{ steps.workstation.outputs.version }} + workstation-installed: 'true' + integration: - needs: lint-unit - runs-on: ubuntu-latest + name: integration (${{ matrix.os }}) + needs: [changes, prepare] + if: needs.changes.outputs.integration == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 30 strategy: fail-fast: false - max-parallel: 4 - matrix: - os: - - almalinux-8 - - almalinux-9 - - almalinux-10 - - amazonlinux-2023 - - centos-stream-9 - - centos-stream-10 - - debian-12 - - debian-13 - - fedora-latest - - opensuse-leap-15 - - oraclelinux-8 - - oraclelinux-9 - - rockylinux-8 - - rockylinux-9 - - rockylinux-10 - - ubuntu-2204 - - ubuntu-2404 - suite: - - basic-site - - default - - mod-auth-cas - - php - - module-template - - mod-wsgi - - ports - - ssl - - install-override - exclude: - # mod_auth_cas requires PCRE1 (libpcre) which is not available on RHEL 10+ - # See: https://github.com/apereo/mod_auth_cas - needs update to support PCRE2 - - os: almalinux-10 - suite: mod-auth-cas - - os: centos-stream-10 - suite: mod-auth-cas - - os: rockylinux-10 - suite: mod-auth-cas - # mod_wsgi package not available on Amazon Linux 2023 - # See: https://github.com/amazonlinux/amazon-linux-2023/issues/532 - - os: amazonlinux-2023 - suite: mod-wsgi - # mod_wsgi idempotency issue on openSUSE Leap 15 - package reinstalls on second run - - os: opensuse-leap-15 - suite: mod-wsgi + max-parallel: 8 + matrix: ${{ fromJSON(needs.prepare.outputs.grouped) }} + env: + CHEF_LICENSE: accept-no-persist + CHEF_VERSION: latest + KITCHEN_LOCAL_YAML: kitchen.dokken.yml + KITCHEN_PRODUCT_NAME: cinc + MULTIPLE_CONVERGE: '2' + KITCHEN_REUSE_IMAGES: 'true' steps: - - name: Check out code - uses: actions/checkout@v7 - - name: Install Cinc Workstation - uses: sous-chefs/.github/.github/actions/install-workstation@9.0.1 - - name: Dokken - uses: actionshub/test-kitchen@main - env: - CHEF_LICENSE: accept-no-persist - CHEF_VERSION: latest - KITCHEN_LOCAL_YAML: kitchen.dokken.yml - KITCHEN_PRODUCT_NAME: cinc + - uses: actions/checkout@v7 + - uses: ./.github/actions/setup-integration with: - suite: ${{ matrix.suite }} - os: ${{ matrix.os }} - - name: Print debug output on failure - if: failure() + dependencies: ${{ needs.prepare.outputs.dependencies }} + dependency-key: ${{ needs.prepare.outputs.dependency-key }} + workstation-version: ${{ needs.prepare.outputs.workstation-version }} + - name: Dokken suites with fresh containers env: - CHEF_VERSION: latest - KITCHEN_LOCAL_YAML: kitchen.dokken.yml - KITCHEN_PRODUCT_NAME: cinc - run: | - set -x - sudo journalctl -l --since today - /usr/bin/kitchen exec ${{ matrix.suite }}-${{ matrix.os }} -c "journalctl -l" + KITCHEN_INSTANCES: ${{ toJSON(matrix.instances) }} + run: python3 .github/ci/integration.py run final: - runs-on: ubuntu-latest - needs: [integration] + runs-on: ubuntu-slim + needs: [changes, lint-unit, prepare, integration] + if: always() steps: - - run: | - echo "Integration result: ${{ needs.integration.result }}" + - name: Require successful checks or an intentional documentation skip + env: + CHANGES: ${{ needs.changes.result }} + LINT: ${{ needs.lint-unit.result }} + REQUIRED: ${{ needs.changes.outputs.integration }} + PREPARE: ${{ needs.prepare.result }} + INTEGRATION: ${{ needs.integration.result }} + run: | + test "$CHANGES" = success + test "$LINT" = success + if [ "$REQUIRED" = true ]; then + test "$PREPARE" = success + test "$INTEGRATION" = success + else + test "$REQUIRED" = false + echo 'Integration skipped: documentation-only changes.' + fi diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index cfdd5f425..dbf07314d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,7 +6,7 @@ name: Mark stale issues and pull requests jobs: stale: - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - uses: actions/stale@v11 with: diff --git a/kitchen.dokken.yml b/kitchen.dokken.yml index f581504f3..dd679cd3d 100644 --- a/kitchen.dokken.yml +++ b/kitchen.dokken.yml @@ -1,6 +1,10 @@ driver: name: dokken privileged: true + # CI runners start fresh; later suites can use the same downloaded base images. + # Containers and suite state are still destroyed after every test. + pull_chef_image: <%= ENV['KITCHEN_REUSE_IMAGES'] != 'true' %> + pull_platform_image: <%= ENV['KITCHEN_REUSE_IMAGES'] != 'true' %> chef_image: "<%= ENV.fetch('KITCHEN_PRODUCT_NAME', 'cinc') == 'cinc' ? 'cincproject/cinc' : 'chef/chef' %>" chef_version: "<%= ENV['CHEF_VERSION'] || (ENV.fetch('KITCHEN_PRODUCT_NAME', 'cinc') == 'cinc' ? 'latest' : 'current') %>" diff --git a/resources/mod_include.rb b/resources/mod_include.rb index 9d364333f..e2bc9b1b9 100644 --- a/resources/mod_include.rb +++ b/resources/mod_include.rb @@ -4,12 +4,10 @@ unified_mode true property :add_type, Hash, - default: { 1 => 'text/html .shtml' }, - description: '' + default: { 1 => 'text/html .shtml' } property :add_output_filter, Hash, - default: { 1 => 'INCLUDES .shtml' }, - description: '' + default: { 1 => 'INCLUDES .shtml' } action :create do template ::File.join(apache_dir, 'mods-available', 'include.conf') do diff --git a/resources/mod_info.rb b/resources/mod_info.rb index 9b5877569..b99e6fb24 100644 --- a/resources/mod_info.rb +++ b/resources/mod_info.rb @@ -4,8 +4,7 @@ unified_mode true property :info_allow_list, [String, Array], - default: %w(127.0.0.1 ::1), - description: '' + default: %w(127.0.0.1 ::1) action :create do template ::File.join(apache_dir, 'mods-available', 'info.conf') do diff --git a/resources/mod_ldap.rb b/resources/mod_ldap.rb index 9c2668f4f..d7869d205 100644 --- a/resources/mod_ldap.rb +++ b/resources/mod_ldap.rb @@ -4,16 +4,13 @@ unified_mode true property :location, String, - default: '/ldap-status', - description: '' + default: '/ldap-status' property :set_handler, String, - default: 'ldap-status', - description: '' + default: 'ldap-status' property :require, String, - default: 'local', - description: '' + default: 'local' action :create do template ::File.join(apache_dir, 'mods-available', 'ldap.conf') do diff --git a/resources/mod_mime.rb b/resources/mod_mime.rb index 4e5d81173..980124f73 100644 --- a/resources/mod_mime.rb +++ b/resources/mod_mime.rb @@ -5,8 +5,7 @@ include Apache2::Cookbook::Helpers property :types_config, String, - default: lazy { default_types_config }, - description: '' + default: lazy { default_types_config } property :add_type, Hash, default: { @@ -15,20 +14,16 @@ 3 => 'application/x-gzip .gz .tgz', 4 => 'application/x-bzip2 .bz2', 5 => 'image/svg+xml svg svgz', - }, - description: '' + } property :add_handler, Hash, - default: { 1 => 'AddHandler type-map var' }, - description: '' + default: { 1 => 'AddHandler type-map var' } property :add_output_filter, Hash, - default: { 1 => 'INCLUDES .shtml' }, - description: '' + default: { 1 => 'INCLUDES .shtml' } property :add_encoding, Hash, - default: { 1 => 'gzip svgz' }, - description: '' + default: { 1 => 'gzip svgz' } property :add_language, Hash, default: {}, diff --git a/resources/mod_mpm_event.rb b/resources/mod_mpm_event.rb index 1bccdcfe5..53d31c3d3 100644 --- a/resources/mod_mpm_event.rb +++ b/resources/mod_mpm_event.rb @@ -4,36 +4,28 @@ unified_mode true property :startservers, Integer, - default: 4, - description: '' + default: 4 property :serverlimit, Integer, - default: 16, - description: '' + default: 16 property :minsparethreads, Integer, - default: 64, - description: '' + default: 64 property :maxsparethreads, Integer, - default: 192, - description: '' + default: 192 property :threadlimit, Integer, - default: 192, - description: '' + default: 192 property :threadsperchild, Integer, - default: 64, - description: '' + default: 64 property :maxrequestworkers, Integer, - default: 1024, - description: '' + default: 1024 property :maxconnectionsperchild, Integer, - default: 0, - description: '' + default: 0 action :create do template ::File.join(apache_dir, 'mods-available', 'mpm_event.conf') do diff --git a/resources/mod_mpm_prefork.rb b/resources/mod_mpm_prefork.rb index ab109254e..bc809c56f 100644 --- a/resources/mod_mpm_prefork.rb +++ b/resources/mod_mpm_prefork.rb @@ -16,16 +16,14 @@ description: 'maximum number of server processes which are kept spare' property :serverlimit, Integer, - default: 256, - description: '' + default: 256 property :maxrequestworkers, Integer, default: 256, description: 'maximum number of server processes allowed to start' property :maxconnectionsperchild, Integer, - default: 10_000, - description: '' + default: 10_000 action :create do template ::File.join(apache_dir, 'mods-available', 'mpm_prefork.conf') do diff --git a/resources/mod_mpm_worker.rb b/resources/mod_mpm_worker.rb index 73d1609b0..f1ad22eee 100644 --- a/resources/mod_mpm_worker.rb +++ b/resources/mod_mpm_worker.rb @@ -32,8 +32,7 @@ description: 'ThreadsPerChild can be changed to this maximum value during a graceful restart. ThreadLimit can only be changed by stopping and starting Apache.' property :serverlimit, Integer, - default: 16, - description: '' + default: 16 action :create do template ::File.join(apache_dir, 'mods-available', 'mpm_worker.conf') do diff --git a/resources/mod_negotiation.rb b/resources/mod_negotiation.rb index 0d2e7510d..cc192a232 100644 --- a/resources/mod_negotiation.rb +++ b/resources/mod_negotiation.rb @@ -4,12 +4,10 @@ unified_mode true property :language_priority, Array, - default: %w( en ca cs da de el eo es et fr he hr it ja ko ltz nl nn no pl pt pt-BR ru sv tr zh-CN zh-TW), - description: '' + default: %w( en ca cs da de el eo es et fr he hr it ja ko ltz nl nn no pl pt pt-BR ru sv tr zh-CN zh-TW) property :force_language_priority, String, - default: 'Prefer Fallback', - description: '' + default: 'Prefer Fallback' action :create do template ::File.join(apache_dir, 'mods-available', 'negotiation.conf') do diff --git a/resources/mod_pagespeed.rb b/resources/mod_pagespeed.rb index cff841bfb..2f72dbcfc 100644 --- a/resources/mod_pagespeed.rb +++ b/resources/mod_pagespeed.rb @@ -5,30 +5,24 @@ include Apache2::Cookbook::Helpers property :apache_user, String, - default: lazy { default_apache_user }, - description: '' + default: lazy { default_apache_user } property :apache_group, String, - default: lazy { default_apache_group }, - description: '' + default: lazy { default_apache_group } property :mod_page_speed, String, equal_to: %w(on off), - default: 'on', - description: '' + default: 'on' property :file_cache_path, String, - default: '/var/cache/mod_pagespeed/', - description: '' + default: '/var/cache/mod_pagespeed/' property :output_filters, Array, - default: ['MOD_PAGESPEED_OUTPUT_FILTER text/html'], - description: '' + default: ['MOD_PAGESPEED_OUTPUT_FILTER text/html'] property :inherit_vhost_config, String, default: 'on', - equal_to: %w(on off), - description: '' + equal_to: %w(on off) property :rewrite_level, String, default: '', @@ -53,13 +47,11 @@ property :file_cache_inode_limit, [String, Integer], default: '500000', - coerce: proc { |m| m.is_a?(Integer) ? m.to_s : m }, - description: '' + coerce: proc { |m| m.is_a?(Integer) ? m.to_s : m } property :avoid_renaming_introspective_javascript, String, equal_to: %w(on off), - default: 'on', - description: '' + default: 'on' property :library, Array, default: [ diff --git a/resources/mod_proxy.rb b/resources/mod_proxy.rb index 6caffab9e..58e4170e0 100644 --- a/resources/mod_proxy.rb +++ b/resources/mod_proxy.rb @@ -4,8 +4,7 @@ unified_mode true property :proxy_requests, String, - default: 'Off', - description: '' + default: 'Off' property :require, String, default: 'all denied', diff --git a/resources/mod_proxy_balancer.rb b/resources/mod_proxy_balancer.rb index b4eed6909..e0e06a5f0 100644 --- a/resources/mod_proxy_balancer.rb +++ b/resources/mod_proxy_balancer.rb @@ -4,12 +4,10 @@ unified_mode true property :status_location, String, - default: '/balancer-manager', - description: '' + default: '/balancer-manager' property :set_handler, String, - default: 'balancer-manager', - description: '' + default: 'balancer-manager' property :require, String, default: 'local', diff --git a/resources/mod_proxy_ftp.rb b/resources/mod_proxy_ftp.rb index 35eb11cfb..94f7877c4 100644 --- a/resources/mod_proxy_ftp.rb +++ b/resources/mod_proxy_ftp.rb @@ -4,8 +4,7 @@ unified_mode true property :proxy_ftp_dir_charset, String, - default: 'UTF-8', - description: '' + default: 'UTF-8' property :proxy_ftp_escape_wildcards, String, equal_to: ['on', 'off', ''], diff --git a/resources/mod_setenvif.rb b/resources/mod_setenvif.rb index 3fca4e115..01e5b988a 100644 --- a/resources/mod_setenvif.rb +++ b/resources/mod_setenvif.rb @@ -19,16 +19,13 @@ '"^XML Spy" redirect-carefully', '"^Dreamweaver-WebDAV-SCM1" redirect-carefully', '"Konqueror/4" redirect-carefully', - ], - description: '' + ] property :browser_match_nocase, Array, - default: [], - description: '' + default: [] property :set_env_if_no_case, Array, - default: [], - description: '' + default: [] action :create do template ::File.join(apache_dir, 'mods-available', 'setenvif.conf') do diff --git a/resources/mod_ssl.rb b/resources/mod_ssl.rb index 511860b9a..937c14c16 100644 --- a/resources/mod_ssl.rb +++ b/resources/mod_ssl.rb @@ -9,59 +9,45 @@ description: 'The name of the mod_ssl package' property :pass_phrase_dialog, String, - default: lazy { default_pass_phrase_dialog }, - description: '' + default: lazy { default_pass_phrase_dialog } property :session_cache, String, - default: lazy { default_session_cache }, - description: '' + default: lazy { default_session_cache } property :session_cache_timeout, String, - default: '300', - description: '' + default: '300' property :cipher_suite, String, - default: 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA', - description: '' + default: 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA' property :honor_cipher_order, String, - default: 'On', - description: '' + default: 'On' property :protocol, String, - default: 'TLSv1.2', - description: '' + default: 'TLSv1.2' property :insecure_renegotiation, String, - default: 'Off', - description: '' + default: 'Off' property :strict_sni_vhost_check, String, - default: 'Off', - description: '' + default: 'Off' property :compression, String, - default: 'Off', - description: '' + default: 'Off' property :use_stapling, String, - default: 'Off', - description: '' + default: 'Off' property :stapling_responder_timeout, String, - default: '5', - description: '' + default: '5' property :stapling_return_responder_errors, String, - default: 'Off', - description: '' + default: 'Off' property :stapling_cache, String, - default: 'shmcb:/var/run/ocsp(128000)', - description: '' + default: 'shmcb:/var/run/ocsp(128000)' -property :directives, Hash, - description: '' +property :directives, Hash action :create do if platform_family?('rhel', 'fedora', 'suse', 'amazon') diff --git a/resources/mod_status.rb b/resources/mod_status.rb index 723452482..a06c4d4f3 100644 --- a/resources/mod_status.rb +++ b/resources/mod_status.rb @@ -4,8 +4,7 @@ unified_mode true property :location, String, - default: '/server-status', - description: '' + default: '/server-status' property :status_allow_list, [String, Array], default: %w(127.0.0.1 ::1), diff --git a/resources/mod_userdir.rb b/resources/mod_userdir.rb index d18b3ce5d..d3a2fea1b 100644 --- a/resources/mod_userdir.rb +++ b/resources/mod_userdir.rb @@ -4,12 +4,10 @@ unified_mode true property :public_html_dir, String, - default: '/home/*/public_html', - description: '' + default: '/home/*/public_html' property :options, String, - default: 'MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec', - description: '' + default: 'MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec' property :allow_override, String, default: 'FileInfo AuthConfig Limit Indexes', diff --git a/resources/module.rb b/resources/module.rb index 2825a8cf0..c801576b6 100644 --- a/resources/module.rb +++ b/resources/module.rb @@ -9,8 +9,7 @@ description: 'The full name of the file' property :path, String, - default: lazy { "#{apache_libexec_dir}/#{mod_name}" }, - description: '' + default: lazy { "#{apache_libexec_dir}/#{mod_name}" } property :identifier, String, default: lazy { "#{name}_module" }, diff --git a/test/cookbooks/test/metadata.rb b/test/cookbooks/test/metadata.rb index a082d3d9d..29e35f811 100644 --- a/test/cookbooks/test/metadata.rb +++ b/test/cookbooks/test/metadata.rb @@ -6,7 +6,7 @@ description 'Testing cookbook for apache2' version '0.0.1' -depends 'apache2' -depends 'apt' +depends 'apache2' +depends 'apt' # depends 'yum-ius' -depends 'php' +depends 'php'