6 Commits
Author SHA1 Message Date
zemion ec3ebd2d3d Release v0.1.17
Module Package Release / publish-packages (push) Successful in 12s
2026-08-05 20:34:03 +02:00
zemion 619af7ecb0 Release v0.1.16
Module Package Release / publish-packages (push) Successful in 12s
2026-08-05 19:52:10 +02:00
zemion e6ca2fbd8b Release v0.1.15
Module Package Release / publish-packages (push) Successful in 11s
2026-08-04 15:10:19 +02:00
zemion 3ba0e5ce40 Make package publication retries hash-safe 2026-08-04 14:32:19 +02:00
zemion 8bdce810f2 Define governed forms runtime profiles 2026-08-04 14:02:03 +02:00
zemion d0678cef9a Harden module package publication 2026-08-04 14:01:56 +02:00
6 changed files with 126 additions and 36 deletions
+83 -22
View File
@@ -14,6 +14,8 @@ on:
jobs:
publish-packages:
runs-on: ubuntu-latest
env:
GITEA_REPOSITORY: ${{ gitea.repository }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
@@ -29,7 +31,6 @@ jobs:
env:
REQUESTED_TAG: ${{ inputs.release_tag }}
TRIGGER_TAG: ${{ gitea.ref_name }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
tag="${REQUESTED_TAG:-$TRIGGER_TAG}"
@@ -43,24 +44,6 @@ jobs:
echo "Release tag is not contained in main" >&2
exit 1
}
python - "$tag" <<'PY'
import fnmatch
import json
import os
import sys
import urllib.request
tag = sys.argv[1]
repository = os.environ["GITEA_REPOSITORY"]
request = urllib.request.Request(
f"{os.environ['GITEA_API_URL']}/repos/{repository}/tag_protections",
headers={"Authorization": f"token {os.environ['GITEA_TOKEN']}"},
)
with urllib.request.urlopen(request, timeout=30) as response:
protections = json.load(response)
if not any(fnmatch.fnmatchcase(tag, item.get("name_pattern", "")) for item in protections):
raise SystemExit(f"Release tag {tag!r} is not covered by repository tag protection")
PY
git checkout --detach "$tag"
printf 'RELEASE_TAG=%s\n' "$tag" >> "$GITEA_ENV"
printf 'SOURCE_DATE_EPOCH=%s\n' "$(git show -s --format=%ct HEAD)" >> "$GITEA_ENV"
@@ -130,7 +113,7 @@ jobs:
const escapedRepository = repository.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const gitTag = specifier.match(
new RegExp(
`^git\\+(?:ssh://git@|https://)git\\.add-ideas\\.de/GovOPlaN/${escapedRepository}\\.git#v([0-9]+\\.[0-9]+\\.[0-9]+)$`,
`^git\\+(?:ssh://git@|https://)git\\.add-ideas\\.de/(?:GovOPlaN|add-ideas)/${escapedRepository}\\.git#v([0-9]+\\.[0-9]+\\.[0-9]+)$`,
),
);
if (gitTag) {
@@ -180,6 +163,78 @@ jobs:
with:
name: module-packages-${{ gitea.ref_name }}
path: dist/package-artifacts.json
- name: Check immutable registry state
shell: bash
env:
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "$PACKAGE_TOKEN"
python - <<'PY'
import hashlib
import json
import os
from pathlib import Path
import tomllib
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
api_root = "https://git.add-ideas.de/api/v1/packages/GovOPlaN"
token = os.environ["PACKAGE_TOKEN"]
def should_publish(kind, name, version, path):
package_url = "/".join(
(api_root, kind, quote(name, safe=""), quote(version, safe=""), "files")
)
request = Request(
package_url,
headers={"Accept": "application/json", "Authorization": f"token {token}"},
)
try:
with urlopen(request, timeout=30) as response:
files = json.load(response)
except HTTPError as exc:
if exc.code == 404:
print(f"{kind} package {name}=={version} is not published yet")
return True
raise
if not isinstance(files, list) or len(files) != 1:
raise SystemExit(
f"immutable {kind} package {name}=={version} has an unexpected file set"
)
expected_sha256 = hashlib.sha256(path.read_bytes()).hexdigest()
if files[0].get("sha256") != expected_sha256:
raise SystemExit(
f"immutable {kind} package {name}=={version} already exists with a different SHA-256"
)
print(f"verified existing {kind} package {name}=={version} ({expected_sha256})")
return False
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
wheels = tuple(Path("dist").glob("*.whl"))
if len(wheels) != 1:
raise SystemExit("release build must contain exactly one wheel")
publish_pypi = should_publish(
"pypi", str(project["name"]), str(project["version"]), wheels[0]
)
tarballs = tuple(Path("dist").glob("*.tgz"))
if len(tarballs) > 1:
raise SystemExit("release build must contain at most one npm package")
publish_npm = False
if tarballs:
webui = json.loads(
Path(".package-webui/package.json").read_text(encoding="utf-8")
)
publish_npm = should_publish(
"npm", str(webui["name"]), str(webui["version"]), tarballs[0]
)
with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file:
env_file.write(f"PUBLISH_PYPI={int(publish_pypi)}\n")
env_file.write(f"PUBLISH_NPM={int(publish_npm)}\n")
PY
- name: Publish wheel and WebUI package
shell: bash
env:
@@ -189,13 +244,17 @@ jobs:
set -euo pipefail
test -n "$PACKAGE_USERNAME"
test -n "$PACKAGE_TOKEN"
if [[ "$PUBLISH_PYPI" == 1 ]]; then
TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \
python -m twine upload --non-interactive \
--repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \
dist/*.whl
else
echo "Exact wheel is already present; skipping immutable retry."
fi
shopt -s nullglob
webui_packages=(dist/*.tgz)
if (( ${#webui_packages[@]} )); then
if (( ${#webui_packages[@]} )) && [[ "$PUBLISH_NPM" == 1 ]]; then
npmrc="$(mktemp)"
trap 'rm -f "$npmrc"' EXIT
chmod 600 "$npmrc"
@@ -203,7 +262,9 @@ jobs:
'@govoplan:registry=https://git.add-ideas.de/api/packages/GovOPlaN/npm/' \
"//git.add-ideas.de/api/packages/GovOPlaN/npm/:_authToken=$PACKAGE_TOKEN" \
> "$npmrc"
NPM_CONFIG_USERCONFIG="$npmrc" npm publish "${webui_packages[0]}" \
NPM_CONFIG_USERCONFIG="$npmrc" npm publish "./${webui_packages[0]}" \
--ignore-scripts --access public \
--registry https://git.add-ideas.de/api/packages/GovOPlaN/npm/
elif (( ${#webui_packages[@]} )); then
echo "Exact WebUI package is already present; skipping immutable retry."
fi
+31 -2
View File
@@ -71,6 +71,35 @@ stores only same-tenant evidence references. Cases and Workflow Engine retain
their own target state; Runtime stores only a permitted same-tenant handoff
reference and status evidence.
## Approved Intake And Evidence Profiles
The product and security profile approved on 2026-08-04 sets the next
implementation boundary:
- authenticated-account and invitation-token intake are the first public entry
profiles;
- anonymous intake is available only through an explicit per-form policy
opt-in, while a pseudonymous profile remains deferred;
- an anonymous submission cannot later be claimed by an identity; an invitation
submission can be linked only with explicit consent and proof of that
invitation;
- invitation tokens are hashed, tenant/form bound, replay safe, rate limited,
and expire after 14 days by default;
- drafts expire after 30 days by default, while every service/form must declare
submitted-data retention explicitly; and
- CAPTCHA remains an optional privacy-approved provider instead of a mandatory
external dependency.
Files is the first attachment provider and retains byte storage, quarantine,
scanning, classification, retention, and legal-hold ownership. Runtime stores
only immutable same-tenant evidence references and must fail closed when a
required item is pending, rejected, expired, unavailable, or unverifiable.
The first native signature profile is an authenticated acknowledgement. It is
not an advanced or qualified electronic signature. Those assurance levels
require a separately selected external trust-service provider and current
provider evidence; a required signature never silently degrades.
## Recovery And Operations
Database recovery restores identities, revisions, and events together. After
@@ -84,8 +113,8 @@ Destructive retirement is blocked while state exists and requires a verified
database snapshot plus an export or retention decision for referenced evidence.
No local generated files are required, so API and worker nodes remain stateless.
Anonymous public intake and concrete file/signature upload adapters remain
product depth. Conditional multi-page definitions are resolved from Forms, and
The approved public-intake and concrete file/signature provider profiles remain
implementation depth. Conditional multi-page definitions are resolved from Forms, and
native Case/Workflow handoffs execute automatically when the exact owner
capability is installed. Additional target kinds remain adapter depth; the
owner and security boundaries no longer depend on those additions.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/forms-runtime",
"version": "0.1.14",
"version": "0.1.17",
"private": true,
"description": "Definition-aware form submissions and service launch for GovOPlaN.",
"type": "module",
+4 -4
View File
@@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-forms-runtime"
version = "0.1.14"
version = "0.1.17"
description = "Definition-aware form submissions and service launch for GovOPlaN."
readme = "README.md"
requires-python = ">=3.12"
license = { file = "LICENSE" }
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.14",
"govoplan-access>=0.1.8",
"govoplan-forms>=0.1.14",
"govoplan-core>=0.1.17",
"govoplan-access>=0.1.17",
"govoplan-forms>=0.1.17",
]
[tool.setuptools.packages.find]
@@ -44,7 +44,7 @@ from govoplan_forms_runtime.backend.service import (
MODULE_ID = "forms_runtime"
MODULE_NAME = "Forms Runtime"
MODULE_VERSION = "0.1.14"
MODULE_VERSION = "0.1.17"
PARTICIPATE_SCOPE = "forms_runtime:submission:participate"
READ_SCOPE = "forms_runtime:workspace:read"
WRITE_SCOPE = "forms_runtime:workspace:write"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/forms-runtime-webui",
"version": "0.1.14",
"version": "0.1.17",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -14,7 +14,7 @@
"./styles/forms-runtime.css": "./src/styles/forms-runtime.css"
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.14",
"@govoplan/core-webui": "^0.1.17",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",