diff --git a/.gitea/workflows/publish-developer-meta-package.yml b/.gitea/workflows/publish-developer-meta-package.yml index 28a620f..ed3aac4 100644 --- a/.gitea/workflows/publish-developer-meta-package.yml +++ b/.gitea/workflows/publish-developer-meta-package.yml @@ -8,6 +8,8 @@ on: jobs: publish-package: runs-on: ubuntu-latest + env: + GITEA_REPOSITORY: ${{ gitea.repository }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: @@ -16,17 +18,12 @@ jobs: with: python-version: "3.12" - name: Validate protected release tag and package version - env: - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | python - <<'PY' - import fnmatch - import json import os from pathlib import Path import subprocess import tomllib - import urllib.request tag = os.environ["GITEA_REF_NAME"] project = tomllib.loads(Path("packages/govoplan-meta/pyproject.toml").read_text(encoding="utf-8"))["project"] @@ -34,14 +31,6 @@ jobs: raise SystemExit("meta-package version does not match the release tag") if subprocess.run(["git", "merge-base", "--is-ancestor", "HEAD", "origin/main"]).returncode: raise SystemExit("release tag is not contained in main") - request = urllib.request.Request( - f"{os.environ['GITEA_API_URL']}/repos/{os.environ['GITEA_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("release tag is not protected") PY - name: Build and publish developer package env: diff --git a/docs/PACKAGE_REGISTRY_RELEASES.md b/docs/PACKAGE_REGISTRY_RELEASES.md index 4d5aca0..56c20ff 100644 --- a/docs/PACKAGE_REGISTRY_RELEASES.md +++ b/docs/PACKAGE_REGISTRY_RELEASES.md @@ -18,24 +18,43 @@ python tools/repo/sync-module-package-workflows.py --check ``` The workflow runs for `v*` tags and may be dispatched manually for an existing -tag. Before building, it verifies that: +tag. The organization preflight verifies that every package repository protects +the `v*` namespace. Before building, the workflow itself verifies that: -- the selected tag is covered by repository tag protection; - the tagged commit is contained in `main`; - the tag, Python project version, and optional WebUI package version agree; - package names remain in the `govoplan-*` and `@govoplan/*-webui` namespaces. +The workflow binds the repository explicitly from the Gitea Actions context. +Do not rely on GitHub-compatible environment variables being injected by the +runner image; Gitea runners may expose only the context values. Gitea 1.24 job +tokens cannot read repository tag-protection settings, so package jobs must not +receive a broad administrator token merely to repeat the organization preflight. +Run the following before the first publication and after repository or tag-rule +changes: + +```bash +python tools/gitea/gitea-configure-package-releases.py +``` + It builds one wheel and, where applicable, one npm tarball. The workflow records the source tag, source commit, filename, size, and SHA-256 in `package-artifacts.json` before publishing. Gitea rejects a second upload of the same package version, so correction requires a new version rather than artifact replacement. +The npm tarball is always published through an explicit local `./dist/...` +path. Without that prefix, npm may interpret a relative tarball name as a Git +package shorthand before it ever contacts the configured registry. + Published WebUI packages contain registry-compatible dependencies only. The workflow converts an internal dependency pinned to a protected `vX.Y.Z` Git tag into the exact `X.Y.Z` registry version and rejects unresolved `file:` or Git dependencies. Repository development metadata may therefore keep local or Git references without leaking them into the published package contract. +Historical `add-ideas` and current `GovOPlaN` organization URLs are accepted +for immutable tagged releases; both normalize to the same exact registry +dependency and no branch or unversioned Git reference is accepted. ## One-time Gitea setup diff --git a/tests/test_module_package_workflows.py b/tests/test_module_package_workflows.py index 3f60d2e..3bcb836 100644 --- a/tests/test_module_package_workflows.py +++ b/tests/test_module_package_workflows.py @@ -17,15 +17,18 @@ class ModulePackageWorkflowTests(unittest.TestCase): META_ROOT / "tools/repo/templates/module-package-release.yml" ).read_text(encoding="utf-8") - self.assertIn("tag_protections", workflow) + self.assertIn("GITEA_REPOSITORY: ${{ gitea.repository }}", workflow) + self.assertNotIn("tag_protections", workflow) + self.assertNotIn("secrets.GITEA_TOKEN", workflow) self.assertIn("git merge-base --is-ancestor", workflow) self.assertIn("does not match", workflow) self.assertIn("package-artifacts.json", workflow) self.assertIn("api/packages/GovOPlaN/pypi", workflow) self.assertIn("api/packages/GovOPlaN/npm", workflow) + self.assertIn('npm publish "./${webui_packages[0]}"', workflow) self.assertIn("GOVOPLAN_PACKAGE_TOKEN", workflow) self.assertIn("must resolve to an exact registry version", workflow) - self.assertIn("git\\\\.add-ideas\\\\.de/GovOPlaN", workflow) + self.assertIn("git\\\\.add-ideas\\\\.de/(?:GovOPlaN|add-ideas)", workflow) self.assertIn("release package identity does not match", workflow) self.assertNotIn("Generic", workflow) @@ -52,6 +55,10 @@ class ModulePackageWorkflowTests(unittest.TestCase): "@govoplan/access-webui": ( "git+ssh://git@git.add-ideas.de/GovOPlaN/" "govoplan-access.git#v0.1.11" + ), + "@govoplan/admin-webui": ( + "git+ssh://git@git.add-ideas.de/add-ideas/" + "govoplan-admin.git#v0.1.8" ) }, } @@ -73,6 +80,9 @@ class ModulePackageWorkflowTests(unittest.TestCase): self.assertEqual( "0.1.11", package["dependencies"]["@govoplan/access-webui"] ) + self.assertEqual( + "0.1.8", package["dependencies"]["@govoplan/admin-webui"] + ) def test_sync_script_only_targets_packageable_govoplan_repositories(self) -> None: namespace: dict[str, object] = { diff --git a/tools/repo/templates/module-package-release.yml b/tools/repo/templates/module-package-release.yml index 3365ff2..69dca09 100644 --- a/tools/repo/templates/module-package-release.yml +++ b/tools/repo/templates/module-package-release.yml @@ -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) { @@ -203,7 +186,7 @@ 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/ fi