Skip to content

Commit 31bc11e

Browse files
authored
Merge pull request #12 from techofourown/feat/converge-catalog-tooling
refactor: converge scripts with canonical catalog-tooling
2 parents 4bb7deb + f441324 commit 31bc11e

File tree

9 files changed

+241
-130
lines changed

9 files changed

+241
-130
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,5 @@ jobs:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- uses: actions/checkout@v4
14-
- uses: rhysd/actionlint@v1.7.11
1514
- uses: oras-project/setup-oras@v1
16-
- name: Validate script syntax
17-
run: bash -n scripts/render-catalog-bundle.sh scripts/check-catalog-bundle-smoke.sh scripts/check-image-refs-exist.sh scripts/check-publish-workflow.sh
18-
- name: Validate catalog-row helper syntax
19-
run: python3 -m py_compile scripts/render-catalog-rows.py
20-
- name: Validate publish workflow invariants
21-
run: bash scripts/check-publish-workflow.sh
22-
- name: Validate and render catalog bundle
23-
run: bash scripts/check-catalog-bundle-smoke.sh
24-
- name: Validate referenced images exist
25-
run: bash scripts/check-image-refs-exist.sh
15+
- run: bash scripts/validate-catalog-repo.sh

.github/workflows/publish-catalog-bundle.yml

Lines changed: 2 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -27,92 +27,14 @@ jobs:
2727
set -euo pipefail
2828
digest="$(oras resolve ghcr.io/techofourown/sw-ourbox-os/platform-contract:edge)"
2929
echo "OURBOX_PLATFORM_CONTRACT_DIGEST=${digest}" >> "${GITHUB_ENV}"
30-
- name: Render catalog bundle
31-
run: bash scripts/render-catalog-bundle.sh
3230
- name: Login to GHCR
3331
run: echo "${GITHUB_TOKEN}" | oras login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin
3432
env:
3533
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36-
- name: Publish bundle
37-
run: |
38-
set -euo pipefail
39-
REF="ghcr.io/techofourown/sw-ourbox-catalog-hello-world:latest"
40-
CATALOG_REF="ghcr.io/techofourown/sw-ourbox-catalog-hello-world:catalog-amd64"
41-
CATALOG_ARTIFACT_TYPE="application/vnd.techofourown.ourbox.application-catalog.catalog.v1"
42-
IMMUTABLE_TAG="sha-${GITHUB_SHA}-run-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
43-
IMMUTABLE_REF="ghcr.io/techofourown/sw-ourbox-catalog-hello-world:${IMMUTABLE_TAG}"
44-
VERSION_TAG="main-${GITHUB_SHA::12}"
45-
CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
46-
oras push \
47-
--artifact-type application/vnd.techofourown.ourbox.application-catalog.v1.tar+gzip \
48-
"${IMMUTABLE_REF}" \
49-
dist/application-catalog-bundle.tar.gz:application/vnd.techofourown.ourbox.application-catalog.v1.tar+gzip
50-
DIGEST="$(oras resolve "${IMMUTABLE_REF}")"
51-
EXISTING_CATALOG=""
52-
rm -rf dist/catalog-existing
53-
if oras pull "${CATALOG_REF}" -o dist/catalog-existing >"${RUNNER_TEMP}/catalog-pull.out" 2>"${RUNNER_TEMP}/catalog-pull.err"; then
54-
EXISTING_CATALOG="$(find dist/catalog-existing -maxdepth 4 -type f -name 'catalog.tsv' | head -n 1 || true)"
55-
elif grep -Eiq 'MANIFEST_UNKNOWN|NAME_UNKNOWN|not found|404' "${RUNNER_TEMP}/catalog-pull.err"; then
56-
echo "No existing catalog index found at ${CATALOG_REF}; creating a new catalog.tsv"
57-
else
58-
echo "Failed to pull existing catalog index ${CATALOG_REF}" >&2
59-
cat "${RUNNER_TEMP}/catalog-pull.err" >&2
60-
exit 1
61-
fi
62-
python3 scripts/render-catalog-rows.py \
63-
--catalog-json catalog/catalog.json \
64-
--profile-env catalog/profile.env \
65-
--images-lock dist/images.lock.json \
66-
--existing-catalog "${EXISTING_CATALOG}" \
67-
--out-catalog dist/catalog.tsv \
68-
--channel stable \
69-
--tag "${IMMUTABLE_TAG}" \
70-
--created "${CREATED}" \
71-
--version "${VERSION_TAG}" \
72-
--revision "${GITHUB_SHA}" \
73-
--arch amd64 \
74-
--artifact-digest "${DIGEST}" \
75-
--pinned-ref "ghcr.io/techofourown/sw-ourbox-catalog-hello-world@${DIGEST}"
76-
oras push \
77-
--artifact-type "${CATALOG_ARTIFACT_TYPE}" \
78-
"${CATALOG_REF}" \
79-
dist/catalog.tsv:text/tab-separated-values
80-
oras tag "${IMMUTABLE_REF}" latest >/dev/null
81-
oras tag "${IMMUTABLE_REF}" stable >/dev/null
82-
oras tag "${IMMUTABLE_REF}" "${VERSION_TAG}" >/dev/null
83-
LATEST_DIGEST="$(oras resolve "${REF}")"
84-
[[ "${LATEST_DIGEST}" == "${DIGEST}" ]] || {
85-
echo "latest tag did not resolve to the published immutable digest" >&2
86-
echo "expected: ${DIGEST}" >&2
87-
echo "actual: ${LATEST_DIGEST}" >&2
88-
exit 1
89-
}
90-
DIGEST="${DIGEST}" MUTABLE_REF="${REF}" IMMUTABLE_REF="${IMMUTABLE_REF}" python3 - <<'PY'
91-
import json
92-
import os
93-
from pathlib import Path
94-
95-
record = {
96-
"schema": 1,
97-
"kind": "ourbox-application-catalog-bundle-publish-record",
98-
"catalog_id": "hello-world",
99-
"catalog_name": "Hello World Catalog",
100-
"mutable_ref": os.environ["MUTABLE_REF"],
101-
"immutable_ref": os.environ["IMMUTABLE_REF"],
102-
"artifact_ref": f"ghcr.io/techofourown/sw-ourbox-catalog-hello-world@{os.environ['DIGEST']}",
103-
"artifact_digest": os.environ["DIGEST"],
104-
"github_sha": os.environ["GITHUB_SHA"],
105-
"github_run_id": os.environ["GITHUB_RUN_ID"],
106-
"github_run_attempt": os.environ["GITHUB_RUN_ATTEMPT"],
107-
}
108-
Path("dist/catalog-bundle.publish-record.json").write_text(
109-
json.dumps(record, indent=2) + "\n",
110-
encoding="utf-8",
111-
)
112-
PY
34+
- run: bash scripts/publish-catalog-bundle.sh
11335
- uses: actions/upload-artifact@v4
11436
with:
115-
name: hello-world-catalog-bundle
37+
name: application-catalog-bundle
11638
path: |
11739
dist/application-catalog-bundle.tar.gz
11840
dist/application-catalog-bundle.tar.gz.sha256

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.DS_Store
22
dist/
3+
__pycache__/

scripts/check-catalog-bundle-smoke.sh

100644100755
Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,25 @@ need_cmd sha256sum
1919

2020
python3 -m py_compile "${ROOT}/scripts/render-catalog-rows.py"
2121

22-
python3 - <<'PY' "${ROOT}/catalog/catalog.json" "${ROOT}/catalog/image-sources.json" "${ROOT}/catalog/profile.env"
22+
WORK_ROOT="${TMP_ROOT}/template-copy"
23+
mkdir -p "${WORK_ROOT}"
24+
cp -R "${ROOT}/catalog" "${WORK_ROOT}/catalog"
25+
cp -R "${ROOT}/scripts" "${WORK_ROOT}/scripts"
26+
27+
python3 - <<'PY' "${WORK_ROOT}/catalog/image-sources.json"
28+
import json
29+
import sys
30+
from pathlib import Path
31+
32+
path = Path(sys.argv[1])
33+
payload = json.loads(path.read_text(encoding="utf-8"))
34+
for entry in payload.get("images", []):
35+
name = str(entry["name"])
36+
entry["ref"] = f"ghcr.io/example-org/example-apps/{name}@sha256:" + ("1" * 64)
37+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
38+
PY
39+
40+
python3 - <<'PY' "${WORK_ROOT}/catalog/catalog.json" "${WORK_ROOT}/catalog/image-sources.json" "${WORK_ROOT}/catalog/profile.env"
2341
import json
2442
import re
2543
import sys
@@ -100,31 +118,31 @@ PY
100118

101119
export OURBOX_PLATFORM_CONTRACT_DIGEST="sha256:0000000000000000000000000000000000000000000000000000000000000000"
102120

103-
bash "${ROOT}/scripts/render-catalog-bundle.sh"
121+
bash "${WORK_ROOT}/scripts/render-catalog-bundle.sh"
104122

105-
test -f "${ROOT}/dist/application-catalog-bundle.tar.gz"
106-
test -f "${ROOT}/dist/application-catalog-bundle.tar.gz.sha256"
107-
test -f "${ROOT}/dist/images.lock.json"
123+
test -f "${WORK_ROOT}/dist/application-catalog-bundle.tar.gz"
124+
test -f "${WORK_ROOT}/dist/application-catalog-bundle.tar.gz.sha256"
125+
test -f "${WORK_ROOT}/dist/images.lock.json"
108126

109-
expected_sha="$(awk 'NF>=1 {print $1; exit}' "${ROOT}/dist/application-catalog-bundle.tar.gz.sha256")"
110-
actual_sha="$(sha256sum "${ROOT}/dist/application-catalog-bundle.tar.gz" | awk '{print $1}')"
127+
expected_sha="$(awk 'NF>=1 {print $1; exit}' "${WORK_ROOT}/dist/application-catalog-bundle.tar.gz.sha256")"
128+
actual_sha="$(sha256sum "${WORK_ROOT}/dist/application-catalog-bundle.tar.gz" | awk '{print $1}')"
111129
[[ "${expected_sha}" == "${actual_sha}" ]] || {
112130
echo "bundle sha mismatch" >&2
113131
exit 1
114132
}
115133

116134
mkdir -p "${TMP_ROOT}/extract"
117-
tar -xzf "${ROOT}/dist/application-catalog-bundle.tar.gz" -C "${TMP_ROOT}/extract"
118-
cmp -s "${ROOT}/catalog/catalog.json" "${TMP_ROOT}/extract/catalog.json"
119-
cmp -s "${ROOT}/dist/images.lock.json" "${TMP_ROOT}/extract/images.lock.json"
120-
cmp -s "${ROOT}/catalog/profile.env" "${TMP_ROOT}/extract/profile.env"
135+
tar -xzf "${WORK_ROOT}/dist/application-catalog-bundle.tar.gz" -C "${TMP_ROOT}/extract"
136+
cmp -s "${WORK_ROOT}/catalog/catalog.json" "${TMP_ROOT}/extract/catalog.json"
137+
cmp -s "${WORK_ROOT}/dist/images.lock.json" "${TMP_ROOT}/extract/images.lock.json"
138+
cmp -s "${WORK_ROOT}/catalog/profile.env" "${TMP_ROOT}/extract/profile.env"
121139

122140
python3 - <<'PY' \
123141
"${TMP_ROOT}/extract/manifest.env" \
124142
"${TMP_ROOT}/extract/profile.env" \
125-
"${ROOT}/catalog/catalog.json" \
126-
"${ROOT}/catalog/image-sources.json" \
127-
"${ROOT}/dist/images.lock.json"
143+
"${WORK_ROOT}/catalog/catalog.json" \
144+
"${WORK_ROOT}/catalog/image-sources.json" \
145+
"${WORK_ROOT}/dist/images.lock.json"
128146
import json
129147
import os
130148
import re
@@ -183,10 +201,10 @@ for image in images_lock["images"]:
183201
raise SystemExit(f"generated image lock used_by mismatch for {image['name']}")
184202
PY
185203

186-
python3 "${ROOT}/scripts/render-catalog-rows.py" \
187-
--catalog-json "${ROOT}/catalog/catalog.json" \
188-
--profile-env "${ROOT}/catalog/profile.env" \
189-
--images-lock "${ROOT}/dist/images.lock.json" \
204+
python3 "${WORK_ROOT}/scripts/render-catalog-rows.py" \
205+
--catalog-json "${WORK_ROOT}/catalog/catalog.json" \
206+
--profile-env "${WORK_ROOT}/catalog/profile.env" \
207+
--images-lock "${WORK_ROOT}/dist/images.lock.json" \
190208
--out-catalog "${TMP_ROOT}/catalog.tsv" \
191209
--channel stable \
192210
--tag "sha-test-run-1" \
@@ -195,10 +213,11 @@ python3 "${ROOT}/scripts/render-catalog-rows.py" \
195213
--revision "deadbeefcafedeadbeefcafedeadbeefcafedead" \
196214
--arch amd64 \
197215
--artifact-digest "sha256:1111111111111111111111111111111111111111111111111111111111111111" \
198-
--pinned-ref "ghcr.io/example/sw-ourbox-catalog-hello-world@sha256:1111111111111111111111111111111111111111111111111111111111111111"
216+
--pinned-ref "ghcr.io/example/sw-ourbox-catalog-example@sha256:1111111111111111111111111111111111111111111111111111111111111111"
199217

200-
python3 - <<'PY' "${TMP_ROOT}/catalog.tsv"
218+
python3 - <<'PY' "${TMP_ROOT}/catalog.tsv" "${WORK_ROOT}/catalog/profile.env" "${WORK_ROOT}/catalog/catalog.json"
201219
import csv
220+
import json
202221
import os
203222
import sys
204223
from pathlib import Path
@@ -207,6 +226,14 @@ rows = list(csv.DictReader(Path(sys.argv[1]).open("r", encoding="utf-8"), delimi
207226
if len(rows) != 1:
208227
raise SystemExit(f"expected one rendered catalog row, got {len(rows)}")
209228
row = rows[0]
229+
profile = {}
230+
for raw_line in Path(sys.argv[2]).read_text(encoding="utf-8").splitlines():
231+
line = raw_line.strip()
232+
if not line or line.startswith("#"):
233+
continue
234+
key, value = line.split("=", 1)
235+
profile[key] = value
236+
catalog = json.loads(Path(sys.argv[3]).read_text(encoding="utf-8"))
210237
expected = {
211238
"channel": "stable",
212239
"tag": "sha-test-run-1",
@@ -215,9 +242,9 @@ expected = {
215242
"revision": "deadbeefcafedeadbeefcafedeadbeefcafedead",
216243
"arch": "amd64",
217244
"platform_contract_digest": os.environ["OURBOX_PLATFORM_CONTRACT_DIGEST"],
218-
"platform_profile": "hello-world",
245+
"platform_profile": catalog["catalog_id"],
219246
"artifact_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
220-
"pinned_ref": "ghcr.io/example/sw-ourbox-catalog-hello-world@sha256:1111111111111111111111111111111111111111111111111111111111111111",
247+
"pinned_ref": "ghcr.io/example/sw-ourbox-catalog-example@sha256:1111111111111111111111111111111111111111111111111111111111111111",
221248
}
222249
for key, expected_value in expected.items():
223250
if row.get(key) != expected_value:

scripts/check-image-refs-exist.sh

100644100755
File mode changed.

scripts/check-publish-workflow.sh

100644100755
Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,23 @@ set -euo pipefail
33

44
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
55
WORKFLOW="${ROOT}/.github/workflows/publish-catalog-bundle.yml"
6+
SCRIPT="${ROOT}/scripts/publish-catalog-bundle.sh"
67

7-
python3 - <<'PY' "${WORKFLOW}"
8+
python3 - <<'PY' "${WORKFLOW}" "${SCRIPT}"
89
import sys
910
from pathlib import Path
1011
1112
workflow = Path(sys.argv[1]).read_text(encoding="utf-8")
12-
lines = {line.strip() for line in workflow.splitlines()}
13-
required = [
13+
script = Path(sys.argv[2]).read_text(encoding="utf-8")
14+
15+
workflow_lines = {line.strip() for line in workflow.splitlines()}
16+
script_lines = {line.strip() for line in script.splitlines()}
17+
18+
workflow_required = [
19+
"bash scripts/publish-catalog-bundle.sh",
20+
]
21+
22+
script_required = [
1423
'IMMUTABLE_TAG="sha-${GITHUB_SHA}-run-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"',
1524
'VERSION_TAG="main-${GITHUB_SHA::12}"',
1625
'"${IMMUTABLE_REF}" \\',
@@ -20,25 +29,34 @@ required = [
2029
'oras tag "${IMMUTABLE_REF}" "${VERSION_TAG}" >/dev/null',
2130
'LATEST_DIGEST="$(oras resolve "${REF}")"',
2231
'[[ "${LATEST_DIGEST}" == "${DIGEST}" ]] || {',
23-
'python3 scripts/render-catalog-rows.py \\',
32+
'python3 "${ROOT}/scripts/render-catalog-rows.py" \\',
2433
'dist/catalog.tsv:text/tab-separated-values',
2534
]
26-
banned = [
35+
36+
script_banned = [
2737
'DIGEST="$(oras resolve "${REF}")"',
2838
'oras tag "${REF}" "${IMMUTABLE_TAG}" >/dev/null',
2939
]
3040
31-
missing = [item for item in required if item not in lines]
32-
unexpected = [item for item in banned if item in lines]
33-
if missing or unexpected:
34-
lines = []
35-
if missing:
36-
lines.append("missing expected workflow invariants:")
37-
lines.extend(f" - {item}" for item in missing)
38-
if unexpected:
39-
lines.append("found banned workflow patterns:")
40-
lines.extend(f" - {item}" for item in unexpected)
41-
raise SystemExit("\n".join(lines))
41+
messages = []
42+
43+
wf_missing = [i for i in workflow_required if not any(i in l for l in workflow_lines)]
44+
if wf_missing:
45+
messages.append("workflow missing expected lines:")
46+
messages.extend(f" - {item}" for item in wf_missing)
47+
48+
sc_missing = [i for i in script_required if i not in script_lines]
49+
if sc_missing:
50+
messages.append("publish-catalog-bundle.sh missing expected invariants:")
51+
messages.extend(f" - {item}" for item in sc_missing)
52+
53+
sc_banned = [i for i in script_banned if i in script_lines]
54+
if sc_banned:
55+
messages.append("publish-catalog-bundle.sh has banned patterns:")
56+
messages.extend(f" - {item}" for item in sc_banned)
57+
58+
if messages:
59+
raise SystemExit("\n".join(messages))
4260
PY
4361

44-
echo "Workflow publish invariants look correct: ${WORKFLOW}"
62+
echo "Workflow and publish script invariants look correct"

0 commit comments

Comments
 (0)