Skip to content

Commit a1fcc5a

Browse files
authored
feat: Merge pull request #17 from elabit/feat/osmatrix
Feat/osmatrix
2 parents 013f6ff + 515b7bb commit a1fcc5a

157 files changed

Lines changed: 5445 additions & 81 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/scripts/sync-examples.sh

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
#!/usr/bin/env bash
2-
# sync-examples.sh — Pushes each examples/<name>/ as a snapshot to robotmk/example-<name>
3-
# and each labs/<name>/ as a snapshot to robotmk/<name>.
2+
# sync-examples.sh — Pushes each examples/<name>/ as a snapshot to robotmk/example-<name>,
3+
# each labs/<name>/ as a snapshot to robotmk/lab-<name>,
4+
# and each os/<slug>/ as a snapshot to robotmk/os-<slug>.
45
#
56
# Requires:
67
# - GH_TOKEN env var with a PAT that has Contents+Administration write on the robotmk org
78
# - gh CLI (available on GitHub Actions runners)
89
# - git configured with user.name and user.email
910
#
1011
# Usage:
11-
# .github/scripts/sync-examples.sh [<example-name>] [--lab <lab-name>]
12+
# .github/scripts/sync-examples.sh [<example-name>] [<lab-name>] [<os-slug>]
1213

1314
set -euo pipefail
1415

1516
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1617
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
1718
EXAMPLES_DIR="${REPO_ROOT}/examples"
1819
LABS_DIR="${REPO_ROOT}/labs"
20+
OS_DIR="${REPO_ROOT}/os"
1921
ORG="robotmk"
2022
EXAMPLE_FILTER="${1:-}"
2123
LAB_FILTER="${2:-}" # passed as second arg from workflow (--lab value handled by caller)
24+
OS_FILTER="${3:-}"
2225
SOURCE_SHA="${GITHUB_SHA:-$(git -C "${REPO_ROOT}" rev-parse HEAD)}"
2326
SOURCE_REPO="${GITHUB_REPOSITORY:-elabit/robotmk-starter}"
2427

@@ -31,6 +34,7 @@ EXCLUDE=(
3134
"output.xml"
3235
"browser"
3336
"playwright-log.txt"
37+
"report.md"
3438
)
3539

3640
# ─── Helper ──────────────────────────────────────────────────────────────────
@@ -101,6 +105,20 @@ build_lab_footer() {
101105
EOF
102106
}
103107

108+
build_os_footer() {
109+
local slug="$1"
110+
cat <<EOF
111+
> ---
112+
>
113+
> **This repository is automatically synced from [${SOURCE_REPO}](https://github.com/${SOURCE_REPO}/tree/main/os/${slug}).**
114+
> Do not edit files here directly — changes will be overwritten on the next sync.
115+
> Last sync: [\`${SOURCE_SHA:0:7}\`](https://github.com/${SOURCE_REPO}/commit/${SOURCE_SHA})
116+
117+
---
118+
119+
EOF
120+
}
121+
104122
prepare_workdir() {
105123
local repo="$1"
106124
local tmpdir="$2"
@@ -244,6 +262,61 @@ sync_lab() {
244262
rm -rf "${tmpdir}"
245263
}
246264

265+
sync_os() {
266+
local name="$1"
267+
local src="${OS_DIR}/${name}"
268+
local repo="${ORG}/os-${name}"
269+
local tmpdir
270+
tmpdir="$(mktemp -d)"
271+
272+
echo ""
273+
echo "══ Syncing os ${name}${repo} ══"
274+
275+
ensure_repo "${repo}" "Robotmk OS install verification target (synced from ${SOURCE_REPO})"
276+
277+
local repo_id
278+
repo_id="$(gh api "repos/${repo}" --jq '.id')"
279+
echo " ✓ Repo ID: ${repo_id}"
280+
281+
local has_devcontainer="false"
282+
[[ -d "${src}/.devcontainer" ]] && has_devcontainer="true"
283+
284+
# Prepare workdir: fetch existing content or init empty for brand-new repos
285+
prepare_workdir "${repo}" "${tmpdir}"
286+
287+
# Wipe tracked content so we do a clean snapshot
288+
if git -C "${tmpdir}" rev-parse HEAD &>/dev/null; then
289+
git -C "${tmpdir}" rm -rf . --quiet
290+
fi
291+
292+
rsync -a "${src}/" "${tmpdir}/"
293+
294+
for item in "${EXCLUDE[@]}"; do
295+
rm -rf "${tmpdir:?}/${item}"
296+
done
297+
298+
# README.md is committed and static (Copier-rendered at generate time, see
299+
# _dev/_shared/README-os.md.jinja) -- unlike examples/labs it isn't
300+
# generated by a separate build step, so it's already present in ${src}.
301+
if [[ -f "${tmpdir}/README.md" ]]; then
302+
prepend_header "${tmpdir}/README.md" "$(build_header "${name}" "${repo_id}" "${has_devcontainer}")"
303+
append_footer "${tmpdir}/README.md" "$(build_os_footer "${name}")"
304+
else
305+
echo " ❌ No README.md found — skipping header/footer injection."
306+
fi
307+
308+
git -C "${tmpdir}" add -A
309+
if git -C "${tmpdir}" diff --cached --quiet; then
310+
echo " No changes — skipping push."
311+
else
312+
git -C "${tmpdir}" commit -m "sync: from ${SOURCE_REPO}@${SOURCE_SHA:0:7}"
313+
git -C "${tmpdir}" push -u origin main
314+
echo " ✓ Pushed."
315+
fi
316+
317+
rm -rf "${tmpdir}"
318+
}
319+
247320
# ─── Main ────────────────────────────────────────────────────────────────────
248321

249322
echo "=== Syncing examples/ ==="
@@ -265,5 +338,16 @@ if [[ -d "${LABS_DIR}" ]]; then
265338
done
266339
fi
267340

341+
echo ""
342+
echo "=== Syncing os/ ==="
343+
if [[ -d "${OS_DIR}" ]]; then
344+
for dir in "${OS_DIR}"/*/; do
345+
name="$(basename "${dir}")"
346+
if [[ -z "${OS_FILTER}" || "${name}" == "${OS_FILTER}" ]]; then
347+
sync_os "${name}"
348+
fi
349+
done
350+
fi
351+
268352
echo ""
269353
echo "Sync complete."

.github/scripts/update_suite_table.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
GITHUB_REPOSITORY — e.g. "myorg/robotmk-starter" (optional, updates badge URL)
1313
"""
1414

15+
import json
1516
import os
1617
import re
1718
import sys
@@ -112,6 +113,38 @@ def build_table(repo_root: Path, parent: str) -> str:
112113
return "\n".join([header, sep] + rows)
113114

114115

116+
def build_os_table(repo_root: Path) -> str:
117+
"""Build the os/ target table, reading each instance's pinned base image
118+
from its already-rendered devcontainer.json rather than versions.env, so
119+
the table reflects what was actually last generated (AD-12: distinct
120+
build function, os/ has no conda.yaml/robot-doc to parse like build_table
121+
expects)."""
122+
header = "| OS | Base Image | Repository Link |"
123+
sep = "|---|---|---|"
124+
rows = []
125+
os_dir = repo_root / "os"
126+
if os_dir.exists():
127+
for slug_dir in sorted(os_dir.iterdir()):
128+
if not slug_dir.is_dir() or slug_dir.name.startswith("."):
129+
continue
130+
slug = slug_dir.name
131+
devcontainer_json = slug_dir / ".devcontainer" / "devcontainer.json"
132+
image = "—"
133+
if devcontainer_json.exists():
134+
# devcontainer.json is JSONC (// comments allowed) -- strip
135+
# line-comments before parsing, same approach run-suites.yml's
136+
# "os" job already uses to read the same file.
137+
text = re.sub(r"//.*", "", devcontainer_json.read_text())
138+
try:
139+
image = json.loads(text).get("image", "—")
140+
except json.JSONDecodeError:
141+
image = "—"
142+
rows.append(
143+
f"| [{slug}](os/{slug}) | `{image}` | [try out](https://github.com/robotmk/os-{slug}) |"
144+
)
145+
return "\n".join([header, sep] + rows)
146+
147+
115148
def replace_between_markers(content: str, start: str, end: str, replacement: str) -> tuple[str, bool]:
116149
pattern = rf"({re.escape(start)}).*?({re.escape(end)})"
117150
new_content, count = re.subn(
@@ -163,7 +196,15 @@ def main():
163196
"<!-- LABS-TABLE-END -->",
164197
build_table(repo_root, "labs"),
165198
)
166-
if t1 or t2 or t3:
199+
# Update os table
200+
content, t4 = replace_between_markers(
201+
content,
202+
"<!-- OS-TABLE-START -->",
203+
"<!-- OS-TABLE-END -->",
204+
build_os_table(repo_root),
205+
)
206+
207+
if t1 or t2 or t3 or t4:
167208
print("Suite tables updated.")
168209
changed = True
169210

.github/workflows/run-suites.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ on:
66
paths:
77
- "examples/**"
88
- "templates/**"
9+
- "os/**"
910
pull_request:
1011
paths:
1112
- "examples/**"
1213
- "templates/**"
14+
- "os/**"
1315
workflow_dispatch:
1416
inputs:
1517
suite:
@@ -154,3 +156,60 @@ jobs:
154156
name: rf-output-${{ steps.suite-env.outputs.safe_name }}-${{ matrix.os }}
155157
path: ${{ matrix.suite }}/log.html
156158
if-no-files-found: ignore
159+
160+
# ── Stage 3: os content type (provision + verify + report) ────────────────
161+
# Independent of the detect-changes/test pair above: exactly one job per OS
162+
# family, added deliberately one epic at a time (Story 1.5 adds debian;
163+
# Stories 2.2/3.2/4.2 each add one more matrix entry), never auto-detected
164+
# from a directory listing the way the examples/templates matrix is.
165+
os:
166+
runs-on: ubuntu-latest
167+
strategy:
168+
fail-fast: false
169+
matrix:
170+
slug: [debian, ubuntu, rhel, sles]
171+
172+
steps:
173+
- uses: actions/checkout@v4
174+
175+
- name: Read pinned image from the generated devcontainer.json
176+
id: os-env
177+
run: |
178+
# devcontainer.json is JSONC (allows // comments, per the Dev
179+
# Container spec) -- jq can't parse that directly, so strip
180+
# line-comments before feeding it to a JSON parser.
181+
IMAGE=$(python3 -c "
182+
import json, re
183+
text = open('os/${{ matrix.slug }}/.devcontainer/devcontainer.json').read()
184+
text = re.sub(r'//.*', '', text)
185+
print(json.loads(text)['image'])
186+
")
187+
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
188+
189+
- name: Provision, verify, and report
190+
run: |
191+
# Mount ONLY os/<slug> (not the full repo root) -- this instance is
192+
# self-contained (its own copy of the shared Ansible role + RF
193+
# suite via populate.yaml, AD-3/AD-4), so this deliberately mirrors
194+
# exactly what a standalone extracted repo would see, proving the
195+
# self-containment property in CI rather than assuming it.
196+
# No devcontainer CLI in CI (AD-9), so its onCreateCommand/
197+
# postCreateCommand split isn't applied automatically here -- chain
198+
# the same two scripts explicitly instead. `&&` gives the identical
199+
# halt-then-propagate semantics the Dev Container lifecycle gives
200+
# the interactive path: postcreate.sh only runs if oncreate.sh
201+
# succeeded, and the final exit code reflects whichever of the two
202+
# actually failed.
203+
docker run --rm \
204+
-v "${{ github.workspace }}/os/${{ matrix.slug }}:/workspace" \
205+
-w "/workspace" \
206+
"${{ steps.os-env.outputs.image }}" \
207+
bash -c "bash .devcontainer/oncreate.sh && bash .devcontainer/postcreate.sh"
208+
209+
- name: Upload install report
210+
if: always()
211+
uses: actions/upload-artifact@v4
212+
with:
213+
name: os-report-${{ matrix.slug }}
214+
path: os/${{ matrix.slug }}/report.md
215+
if-no-files-found: ignore

.github/workflows/sync-examples.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ on:
1717
description: "Single lab to sync (leave empty for all)"
1818
required: false
1919
default: ""
20+
os:
21+
description: "Single os/ slug to sync (leave empty for all)"
22+
required: false
23+
default: ""
2024

2125
jobs:
2226
sync:
@@ -40,4 +44,4 @@ jobs:
4044
GITHUB_SHA: ${{ github.sha }}
4145
GITHUB_REPOSITORY: ${{ github.repository }}
4246
run: |
43-
bash .github/scripts/sync-examples.sh "${{ github.event.inputs.example }}" "${{ github.event.inputs.lab }}"
47+
bash .github/scripts/sync-examples.sh "${{ github.event.inputs.example }}" "${{ github.event.inputs.lab }}" "${{ github.event.inputs.os }}"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ report.html
1515
output.xml
1616
browser/screenshot/
1717
playwright-log.txt
18+
os/*/report.md
1819

1920
# RCC holotree (local environment cache)
2021
.robocorp/
@@ -36,3 +37,4 @@ nohup.out
3637

3738
__pycache__/*
3839
*.pyc
40+
.claude

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ To try them out, just click on "*try out*" which opens the repository, where you
8888

8989
---
9090

91+
### 🐧 Folder "/os"
92+
93+
Proof that each target OS can be provisioned from a stock install (via Ansible, in a devcontainer) and actually run a Robot Framework web test end-to-end — not just "packages installed", but "the browser test passes".
94+
95+
<!-- OS-TABLE-START -->
96+
97+
| OS | Base Image | Repository Link |
98+
|---|---|---|
99+
| [debian](os/debian) | `debian:13` | [try out](https://github.com/robotmk/os-debian) |
100+
| [rhel](os/rhel) | `rockylinux/rockylinux:10` | [try out](https://github.com/robotmk/os-rhel) |
101+
| [sles](os/sles) | `registry.suse.com/suse/sle15:15.7` | [try out](https://github.com/robotmk/os-sles) |
102+
| [ubuntu](os/ubuntu) | `ubuntu:24.04` | [try out](https://github.com/robotmk/os-ubuntu) |
103+
104+
<!-- OS-TABLE-END -->
105+
106+
---
107+
91108
## For maintainers
92109

93110
Examples and templates are generated from Copier sources in `_dev/`.

TODO.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Labs README shoul dhave a clear instruction (shown in VS Code after start)
2+
-

0 commit comments

Comments
 (0)