unaltraweb 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Makefile +57 -17
- data/README.md +35 -14
- data/docs/_documentation/en/02-tools.md +2 -2
- data/docs/_documentation/en/03-usage.md +1 -1
- data/docs/_documentation/en/06-github-web-editing.md +1 -1
- data/docs/_documentation/en/13-unaltremanual.md +11 -4
- data/docs/_documentation/en/31-template.md +1 -1
- data/docs/_documentation/en/32-development.md +1 -1
- data/docs/_documentation/en/40-distribution.md +39 -22
- data/docs/_documentation/en/42-docker-image.md +20 -10
- data/docs/agents/action-prompts/00-start-site-session.txt +9 -7
- data/docs/agents/mcp-contract.md +12 -6
- data/scripts/manual/templates/manual.tex +4 -0
- data/scripts/test_reproducible_jekyll_build.py +1 -1
- data/scripts/test_wheel_install.py +47 -5
- data/scripts/unaltraweb-mcp-bootstrap.sh +19 -1
- data/scripts/validate_distribution.py +1 -1
- data/scripts/validate_workflows.py +290 -5
- data/scripts/verify_package_publish.py +414 -0
- data/scripts/web_captures/render.py +1 -1
- data/src/unaltraweb_mcp/component-contract.json +29 -29
- metadata +3 -2
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import copy
|
|
7
|
+
import hashlib
|
|
7
8
|
import json
|
|
8
9
|
import re
|
|
9
10
|
from pathlib import Path
|
|
@@ -43,7 +44,14 @@ REVIEWED_ACTIONS = {
|
|
|
43
44
|
"docker/metadata-action": "c299e40c65443455700f0fdfc63efafe5b349051",
|
|
44
45
|
"docker/setup-buildx-action": "8d2750c68a42422c14e847fe6c8ac0403b4cbd6f",
|
|
45
46
|
"ruby/setup-ruby": "95ef2b042f9d7a56d8268cba8559e2842e2ad01b",
|
|
47
|
+
"rubygems/configure-rubygems-credentials": "dc5a8d8553e6ee01fc26761a49e99e733d17954a",
|
|
46
48
|
}
|
|
49
|
+
PYPI_PUBLISH_IMAGE = (
|
|
50
|
+
"docker://ghcr.io/pypa/gh-action-pypi-publish"
|
|
51
|
+
"@sha256:a68d05519f6d7e47372aeaddab80b851b69afa89be179ec41775c72c4e3ab2d5"
|
|
52
|
+
)
|
|
53
|
+
# Canonical JSON digest of the complete unprivileged verification job.
|
|
54
|
+
PACKAGE_PUBLISH_VERIFY_POLICY_SHA256 = "e8d99c35040770a2b00d3c62497da637739e794310b253d1893078d048d51c48"
|
|
47
55
|
IMAGE_WORKFLOWS = {
|
|
48
56
|
"compute-images.yml",
|
|
49
57
|
"docker-image.yml",
|
|
@@ -51,7 +59,13 @@ IMAGE_WORKFLOWS = {
|
|
|
51
59
|
"web-capture-image.yml",
|
|
52
60
|
}
|
|
53
61
|
PROMOTED_CORE_IMAGE_WORKFLOWS = {"compute-images.yml", "docker-image.yml", "web-capture-image.yml"}
|
|
54
|
-
FULLY_PINNED_WORKFLOWS = IMAGE_WORKFLOWS | {
|
|
62
|
+
FULLY_PINNED_WORKFLOWS = IMAGE_WORKFLOWS | {
|
|
63
|
+
"ci.yml",
|
|
64
|
+
"package-prepare.yml",
|
|
65
|
+
"package-publish.yml",
|
|
66
|
+
"site-deploy.yml",
|
|
67
|
+
"site-release.yml",
|
|
68
|
+
}
|
|
55
69
|
|
|
56
70
|
|
|
57
71
|
class WorkflowLoader(yaml.SafeLoader):
|
|
@@ -221,6 +235,12 @@ def validate_workflows(root: Path = WORKFLOW_ROOT) -> list[str]:
|
|
|
221
235
|
uses = step.get("uses")
|
|
222
236
|
if not isinstance(uses, str) or uses.startswith("./"):
|
|
223
237
|
continue
|
|
238
|
+
if uses.startswith("docker://"):
|
|
239
|
+
if name in FULLY_PINNED_WORKFLOWS and re.fullmatch(
|
|
240
|
+
r"docker://[^@]+@sha256:[0-9a-f]{64}", uses
|
|
241
|
+
) is None:
|
|
242
|
+
errors.append(f"{name}:{job_name}: container action must use an immutable digest: {uses}")
|
|
243
|
+
continue
|
|
224
244
|
action, separator, revision = uses.rpartition("@")
|
|
225
245
|
if not separator:
|
|
226
246
|
errors.append(f"{name}:{job_name}: action has no revision: {uses}")
|
|
@@ -228,8 +248,9 @@ def validate_workflows(root: Path = WORKFLOW_ROOT) -> list[str]:
|
|
|
228
248
|
reviewed = REVIEWED_ACTIONS.get(action)
|
|
229
249
|
if reviewed and revision != reviewed:
|
|
230
250
|
errors.append(f"{name}:{job_name}: {action} must use reviewed SHA {reviewed}")
|
|
231
|
-
if name in FULLY_PINNED_WORKFLOWS
|
|
232
|
-
|
|
251
|
+
if name in FULLY_PINNED_WORKFLOWS:
|
|
252
|
+
if not FULL_SHA.fullmatch(revision):
|
|
253
|
+
errors.append(f"{name}:{job_name}: action must use a full commit SHA: {uses}")
|
|
233
254
|
if action == "actions/checkout" and step.get("with", {}).get("persist-credentials") is not False:
|
|
234
255
|
errors.append(f"{name}:{job_name}: checkout must set persist-credentials: false")
|
|
235
256
|
|
|
@@ -607,9 +628,17 @@ def validate_workflows(root: Path = WORKFLOW_ROOT) -> list[str]:
|
|
|
607
628
|
"MCP_IMAGE": (
|
|
608
629
|
"ghcr.io/dosquartsdedocs/unaltraweb-mcp@"
|
|
609
630
|
"${{ needs.build-candidates.outputs.mcp_digest }}"
|
|
610
|
-
)
|
|
631
|
+
),
|
|
632
|
+
"MCP_SMOKE_MANUAL_PDF_IMAGE": (
|
|
633
|
+
"ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf@"
|
|
634
|
+
"${{ needs.build-candidates.outputs.manual_pdf_digest }}"
|
|
635
|
+
),
|
|
611
636
|
},
|
|
612
|
-
[
|
|
637
|
+
[
|
|
638
|
+
"mcp-smoke-prebuilt",
|
|
639
|
+
'MCP_IMAGE="$MCP_IMAGE"',
|
|
640
|
+
'MCP_SMOKE_MANUAL_PDF_IMAGE="$MCP_SMOKE_MANUAL_PDF_IMAGE"',
|
|
641
|
+
],
|
|
613
642
|
),
|
|
614
643
|
"Build docs with exact runtime": (
|
|
615
644
|
{
|
|
@@ -1027,6 +1056,24 @@ def validate_workflows(root: Path = WORKFLOW_ROOT) -> list[str]:
|
|
|
1027
1056
|
manual_pdf_tests = str(_named_step(ci_docker, "Test all manual PDF integrations").get("run") or "")
|
|
1028
1057
|
if "-m unittest discover -s test/manual_pdf -p 'test_*_integration.py'" not in manual_pdf_tests:
|
|
1029
1058
|
errors.append("ci.yml: manual PDF image must run every test_*_integration.py suite")
|
|
1059
|
+
mcp_smoke = _named_step(ci_docker, "Smoke test MCP")
|
|
1060
|
+
mcp_smoke_run = str(mcp_smoke.get("run") or "")
|
|
1061
|
+
if (
|
|
1062
|
+
mcp_smoke.get("env") != {
|
|
1063
|
+
"MCP_IMAGE": "unaltraweb-mcp:ci",
|
|
1064
|
+
"MCP_SMOKE_MANUAL_PDF_IMAGE": "unaltraweb-manual-pdf:ci",
|
|
1065
|
+
}
|
|
1066
|
+
or "continue-on-error" in mcp_smoke
|
|
1067
|
+
or not all(
|
|
1068
|
+
marker in mcp_smoke_run
|
|
1069
|
+
for marker in [
|
|
1070
|
+
"mcp-smoke-prebuilt",
|
|
1071
|
+
'MCP_IMAGE="$MCP_IMAGE"',
|
|
1072
|
+
'MCP_SMOKE_MANUAL_PDF_IMAGE="$MCP_SMOKE_MANUAL_PDF_IMAGE"',
|
|
1073
|
+
]
|
|
1074
|
+
)
|
|
1075
|
+
):
|
|
1076
|
+
errors.append("ci.yml: MCP smoke must pair MCP and manual PDF images built from the same checkout")
|
|
1030
1077
|
|
|
1031
1078
|
package = workflows.get("package-prepare.yml", {})
|
|
1032
1079
|
package_text = json.dumps(package)
|
|
@@ -1041,6 +1088,244 @@ def validate_workflows(root: Path = WORKFLOW_ROOT) -> list[str]:
|
|
|
1041
1088
|
if package.get("permissions", {}).get("contents") != "read" or len(package.get("permissions", {})) != 1:
|
|
1042
1089
|
errors.append("package-prepare.yml: candidate preparation must have read-only repository permissions")
|
|
1043
1090
|
|
|
1091
|
+
package_publish = workflows.get("package-publish.yml")
|
|
1092
|
+
if package_publish is None:
|
|
1093
|
+
errors.append("missing trusted language-package publication workflow: package-publish.yml")
|
|
1094
|
+
else:
|
|
1095
|
+
if set(package_publish) != {"name", "on", "permissions", "concurrency", "jobs"}:
|
|
1096
|
+
errors.append("package-publish.yml: workflow contains authority outside the reviewed structure")
|
|
1097
|
+
publish_on = package_publish.get("on", {})
|
|
1098
|
+
if set(publish_on) != {"workflow_dispatch"}:
|
|
1099
|
+
errors.append("package-publish.yml: package publication must be workflow_dispatch-only")
|
|
1100
|
+
publish_inputs = publish_on.get("workflow_dispatch", {}).get("inputs", {})
|
|
1101
|
+
expected_publish_inputs = {
|
|
1102
|
+
"publisher_sha": {"required": True, "type": "string"},
|
|
1103
|
+
"release_tag": {"required": True, "type": "string"},
|
|
1104
|
+
"release_tag_object": {"required": True, "type": "string"},
|
|
1105
|
+
"receipt_sha256": {"required": True, "type": "string"},
|
|
1106
|
+
"package_run_id": {"required": True, "type": "string"},
|
|
1107
|
+
"publish_target": {
|
|
1108
|
+
"required": True,
|
|
1109
|
+
"type": "choice",
|
|
1110
|
+
"default": "all",
|
|
1111
|
+
"options": ["all", "pypi", "rubygems"],
|
|
1112
|
+
},
|
|
1113
|
+
}
|
|
1114
|
+
if not isinstance(publish_inputs, dict) or set(publish_inputs) != set(expected_publish_inputs):
|
|
1115
|
+
errors.append("package-publish.yml: dispatch input inventory differs from policy")
|
|
1116
|
+
else:
|
|
1117
|
+
for input_name, expected in expected_publish_inputs.items():
|
|
1118
|
+
actual = publish_inputs.get(input_name, {})
|
|
1119
|
+
if any(actual.get(key) != value for key, value in expected.items()):
|
|
1120
|
+
errors.append(f"package-publish.yml: {input_name} input differs from policy")
|
|
1121
|
+
if package_publish.get("permissions") != {}:
|
|
1122
|
+
errors.append("package-publish.yml: workflow must grant no default permissions")
|
|
1123
|
+
if package_publish.get("concurrency") != {
|
|
1124
|
+
"group": "package-publish-${{ inputs.release_tag }}",
|
|
1125
|
+
"cancel-in-progress": False,
|
|
1126
|
+
}:
|
|
1127
|
+
errors.append("package-publish.yml: publication must use release-scoped non-cancelling concurrency")
|
|
1128
|
+
|
|
1129
|
+
publish_jobs = package_publish.get("jobs", {})
|
|
1130
|
+
if set(publish_jobs) != {"verify", "publish-pypi", "publish-rubygems"}:
|
|
1131
|
+
errors.append("package-publish.yml: workflow must contain only verify and two registry jobs")
|
|
1132
|
+
verify_job = publish_jobs.get("verify", {})
|
|
1133
|
+
verify_policy_sha256 = hashlib.sha256(
|
|
1134
|
+
json.dumps(verify_job, sort_keys=True, separators=(",", ":")).encode()
|
|
1135
|
+
).hexdigest()
|
|
1136
|
+
if verify_policy_sha256 != PACKAGE_PUBLISH_VERIFY_POLICY_SHA256:
|
|
1137
|
+
errors.append("package-publish.yml: verification job differs from the exact reviewed structure")
|
|
1138
|
+
if set(verify_job) != {"runs-on", "timeout-minutes", "permissions", "steps"}:
|
|
1139
|
+
errors.append("package-publish.yml: verification job structure differs from policy")
|
|
1140
|
+
if verify_job.get("runs-on") != "ubuntu-24.04" or verify_job.get("timeout-minutes") != 15:
|
|
1141
|
+
errors.append("package-publish.yml: verification job must use the reviewed hosted runner boundary")
|
|
1142
|
+
if verify_job.get("permissions") != {"actions": "read", "contents": "read"}:
|
|
1143
|
+
errors.append("package-publish.yml: verification job must have only actions/content read access")
|
|
1144
|
+
verify_steps = verify_job.get("steps", [])
|
|
1145
|
+
verify_names = [str(step.get("name") or "") for step in verify_steps if isinstance(step, dict)]
|
|
1146
|
+
expected_verify_names = [
|
|
1147
|
+
"Validate publication request",
|
|
1148
|
+
"Checkout reviewed publisher",
|
|
1149
|
+
"Checkout exact release tag",
|
|
1150
|
+
"Validate exact release checkout",
|
|
1151
|
+
"Authorize package candidate run",
|
|
1152
|
+
"Download recorded package artifact",
|
|
1153
|
+
"Verify and stage recorded package candidates",
|
|
1154
|
+
"Upload verified wheel",
|
|
1155
|
+
"Upload verified gem",
|
|
1156
|
+
]
|
|
1157
|
+
if verify_names != expected_verify_names:
|
|
1158
|
+
errors.append("package-publish.yml: verification step inventory or order differs from policy")
|
|
1159
|
+
verify_text = json.dumps(verify_job)
|
|
1160
|
+
for marker in [
|
|
1161
|
+
"refs/heads/${DEFAULT_BRANCH}",
|
|
1162
|
+
"PUBLISHER_SHA",
|
|
1163
|
+
"GITHUB_SHA",
|
|
1164
|
+
"RELEASE_TAG_OBJECT",
|
|
1165
|
+
"RECEIPT_SHA256",
|
|
1166
|
+
"verify_package_publish.py release",
|
|
1167
|
+
"--receipt-sha256",
|
|
1168
|
+
"/actions/runs/${PACKAGE_RUN_ID}",
|
|
1169
|
+
"/actions/runs/${PACKAGE_RUN_ID}/artifacts?per_page=100",
|
|
1170
|
+
"release-candidates.json",
|
|
1171
|
+
"verify_package_publish.py authorize",
|
|
1172
|
+
"verify_package_publish.py stage",
|
|
1173
|
+
"artifact-ids",
|
|
1174
|
+
"overwrite",
|
|
1175
|
+
]:
|
|
1176
|
+
if marker not in verify_text:
|
|
1177
|
+
errors.append(f"package-publish.yml: missing unprivileged package verification gate {marker}")
|
|
1178
|
+
for forbidden in ["id-token", "gem push", "twine upload", "gh-action-pypi-publish", "configure-rubygems-credentials"]:
|
|
1179
|
+
if forbidden in verify_text:
|
|
1180
|
+
errors.append(f"package-publish.yml: verification job contains publication authority {forbidden}")
|
|
1181
|
+
verify_run_text = _run_text(verify_job)
|
|
1182
|
+
for forbidden in ["pip install", "make -C release-source", "make --no-print-directory -C release-source", "release-source/scripts/"]:
|
|
1183
|
+
if forbidden in verify_run_text:
|
|
1184
|
+
errors.append(f"package-publish.yml: verification job must not execute release source: {forbidden}")
|
|
1185
|
+
|
|
1186
|
+
publisher_checkout = _named_step(verify_job, "Checkout reviewed publisher")
|
|
1187
|
+
release_checkout = _named_step(verify_job, "Checkout exact release tag")
|
|
1188
|
+
if publisher_checkout.get("with") != {
|
|
1189
|
+
"ref": "${{ github.sha }}",
|
|
1190
|
+
"path": ".package-publisher",
|
|
1191
|
+
"persist-credentials": False,
|
|
1192
|
+
}:
|
|
1193
|
+
errors.append("package-publish.yml: publisher checkout must use the reviewed workflow SHA")
|
|
1194
|
+
if release_checkout.get("with") != {
|
|
1195
|
+
"fetch-depth": 0,
|
|
1196
|
+
"ref": "refs/tags/${{ inputs.release_tag }}",
|
|
1197
|
+
"path": "release-source",
|
|
1198
|
+
"persist-credentials": False,
|
|
1199
|
+
}:
|
|
1200
|
+
errors.append("package-publish.yml: release checkout must use the exact tag with full history")
|
|
1201
|
+
expected_release_validation = (
|
|
1202
|
+
"python3 .package-publisher/scripts/verify_package_publish.py release \\\n"
|
|
1203
|
+
" --repository-dir release-source \\\n"
|
|
1204
|
+
" --receipt release-source/release-candidates.json \\\n"
|
|
1205
|
+
" --release-tag \"$RELEASE_TAG\" \\\n"
|
|
1206
|
+
" --tag-object \"$RELEASE_TAG_OBJECT\" \\\n"
|
|
1207
|
+
" --receipt-sha256 \"$RECEIPT_SHA256\" \\\n"
|
|
1208
|
+
" --default-branch \"$DEFAULT_BRANCH\""
|
|
1209
|
+
)
|
|
1210
|
+
if str(_named_step(verify_job, "Validate exact release checkout").get("run") or "").strip() != expected_release_validation:
|
|
1211
|
+
errors.append("package-publish.yml: release validation must use only the reviewed publisher verifier")
|
|
1212
|
+
download_candidate = _named_step(verify_job, "Download recorded package artifact")
|
|
1213
|
+
if download_candidate.get("with") != {
|
|
1214
|
+
"artifact-ids": "${{ steps.authorize.outputs.artifact-id }}",
|
|
1215
|
+
"github-token": "${{ github.token }}",
|
|
1216
|
+
"repository": "${{ github.repository }}",
|
|
1217
|
+
"run-id": "${{ inputs.package_run_id }}",
|
|
1218
|
+
"path": "incoming",
|
|
1219
|
+
"merge-multiple": True,
|
|
1220
|
+
}:
|
|
1221
|
+
errors.append("package-publish.yml: candidate download must use the authorized artifact ID and run")
|
|
1222
|
+
expected_stage = (
|
|
1223
|
+
"python3 .package-publisher/scripts/verify_package_publish.py stage \\\n"
|
|
1224
|
+
" --receipt release-source/release-candidates.json \\\n"
|
|
1225
|
+
" --release-tag \"$RELEASE_TAG\" \\\n"
|
|
1226
|
+
" --receipt-sha256 \"$RECEIPT_SHA256\" \\\n"
|
|
1227
|
+
" --candidate-dir incoming \\\n"
|
|
1228
|
+
" --output-dir verified"
|
|
1229
|
+
)
|
|
1230
|
+
if str(_named_step(verify_job, "Verify and stage recorded package candidates").get("run") or "").strip() != expected_stage:
|
|
1231
|
+
errors.append("package-publish.yml: candidate staging command differs from policy")
|
|
1232
|
+
expected_verified_uploads = {
|
|
1233
|
+
"Upload verified wheel": {
|
|
1234
|
+
"name": "verified-pypi-${{ inputs.release_tag }}-${{ github.run_id }}",
|
|
1235
|
+
"path": "verified/pypi/",
|
|
1236
|
+
"if-no-files-found": "error",
|
|
1237
|
+
"compression-level": 0,
|
|
1238
|
+
"overwrite": False,
|
|
1239
|
+
"retention-days": 7,
|
|
1240
|
+
},
|
|
1241
|
+
"Upload verified gem": {
|
|
1242
|
+
"name": "verified-rubygems-${{ inputs.release_tag }}-${{ github.run_id }}",
|
|
1243
|
+
"path": "verified/rubygems/",
|
|
1244
|
+
"if-no-files-found": "error",
|
|
1245
|
+
"compression-level": 0,
|
|
1246
|
+
"overwrite": False,
|
|
1247
|
+
"retention-days": 7,
|
|
1248
|
+
},
|
|
1249
|
+
}
|
|
1250
|
+
for step_name, expected_with in expected_verified_uploads.items():
|
|
1251
|
+
if _named_step(verify_job, step_name).get("with") != expected_with:
|
|
1252
|
+
errors.append(f"package-publish.yml: {step_name} must preserve the verified same-run artifact")
|
|
1253
|
+
|
|
1254
|
+
expected_registry_jobs = {
|
|
1255
|
+
"publish-pypi": {
|
|
1256
|
+
"needs": "verify",
|
|
1257
|
+
"if": "${{ inputs.publish_target == 'all' || inputs.publish_target == 'pypi' }}",
|
|
1258
|
+
"runs-on": "ubuntu-24.04",
|
|
1259
|
+
"timeout-minutes": 10,
|
|
1260
|
+
"environment": "pypi",
|
|
1261
|
+
"permissions": {"actions": "read", "id-token": "write"},
|
|
1262
|
+
"steps": [
|
|
1263
|
+
{
|
|
1264
|
+
"name": "Download verified wheel",
|
|
1265
|
+
"uses": "actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093",
|
|
1266
|
+
"with": {
|
|
1267
|
+
"name": "verified-pypi-${{ inputs.release_tag }}-${{ github.run_id }}",
|
|
1268
|
+
"path": "dist",
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
"name": "Publish exact wheel to PyPI",
|
|
1273
|
+
"uses": PYPI_PUBLISH_IMAGE,
|
|
1274
|
+
"env": {
|
|
1275
|
+
"INPUT_USER": "__token__",
|
|
1276
|
+
"INPUT_PASSWORD": "",
|
|
1277
|
+
"INPUT_REPOSITORY_URL": "https://upload.pypi.org/legacy/",
|
|
1278
|
+
"INPUT_PACKAGES_DIR": "dist",
|
|
1279
|
+
"INPUT_VERIFY_METADATA": "true",
|
|
1280
|
+
"INPUT_SKIP_EXISTING": "false",
|
|
1281
|
+
"INPUT_VERBOSE": "true",
|
|
1282
|
+
"INPUT_PRINT_HASH": "true",
|
|
1283
|
+
"INPUT_ATTESTATIONS": "true",
|
|
1284
|
+
},
|
|
1285
|
+
},
|
|
1286
|
+
],
|
|
1287
|
+
},
|
|
1288
|
+
"publish-rubygems": {
|
|
1289
|
+
"needs": "verify",
|
|
1290
|
+
"if": "${{ inputs.publish_target == 'all' || inputs.publish_target == 'rubygems' }}",
|
|
1291
|
+
"runs-on": "ubuntu-24.04",
|
|
1292
|
+
"timeout-minutes": 10,
|
|
1293
|
+
"environment": "rubygems",
|
|
1294
|
+
"permissions": {"actions": "read", "id-token": "write"},
|
|
1295
|
+
"steps": [
|
|
1296
|
+
{
|
|
1297
|
+
"name": "Download verified gem",
|
|
1298
|
+
"uses": "actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093",
|
|
1299
|
+
"with": {
|
|
1300
|
+
"name": "verified-rubygems-${{ inputs.release_tag }}-${{ github.run_id }}",
|
|
1301
|
+
"path": "gem-dist",
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
"name": "Configure short-lived RubyGems credentials",
|
|
1306
|
+
"uses": "rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a",
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
"name": "Publish exact gem to RubyGems",
|
|
1310
|
+
"shell": "bash",
|
|
1311
|
+
"run": (
|
|
1312
|
+
"set -euo pipefail\n"
|
|
1313
|
+
"shopt -s nullglob\n"
|
|
1314
|
+
"candidates=(gem-dist/*.gem)\n"
|
|
1315
|
+
"if [[ \"${#candidates[@]}\" -ne 1 ]]; then\n"
|
|
1316
|
+
" printf 'Expected exactly one verified gem, found %s\\n' \"${#candidates[@]}\" >&2\n"
|
|
1317
|
+
" exit 1\n"
|
|
1318
|
+
"fi\n"
|
|
1319
|
+
"gem push --host https://rubygems.org \"${candidates[0]}\"\n"
|
|
1320
|
+
),
|
|
1321
|
+
},
|
|
1322
|
+
],
|
|
1323
|
+
},
|
|
1324
|
+
}
|
|
1325
|
+
for job_name, expected_job in expected_registry_jobs.items():
|
|
1326
|
+
if publish_jobs.get(job_name) != expected_job:
|
|
1327
|
+
errors.append(f"package-publish.yml:{job_name}: privileged registry job differs from exact policy")
|
|
1328
|
+
|
|
1044
1329
|
deploy = workflows.get("site-deploy.yml")
|
|
1045
1330
|
if deploy is None:
|
|
1046
1331
|
errors.append("missing reusable latest site workflow: site-deploy.yml")
|