diff --git a/VERSION b/VERSION index ee6cdce..b616048 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 +0.6.2 diff --git a/actions/release-authorization/action.yml b/actions/release-authorization/action.yml index cbeaa72..edcdaac 100644 --- a/actions/release-authorization/action.yml +++ b/actions/release-authorization/action.yml @@ -64,10 +64,10 @@ runs: { echo "## Release authorization" echo - echo "- Actor: `${RELEASE_ACTOR}`" - echo "- Required: `${RELEASE_MINIMUM_PERMISSION}`" - echo "- Actual: `${actual_permission}`" - echo "- Authorized: **${authorized}**" + printf -- '- Actor: `%s`\n' "${RELEASE_ACTOR}" + printf -- '- Required: `%s`\n' "${RELEASE_MINIMUM_PERMISSION}" + printf -- '- Actual: `%s`\n' "${actual_permission}" + printf -- '- Authorized: **%s**\n' "${authorized}" } >> "${GITHUB_STEP_SUMMARY}" if [[ "${authorized}" != "true" ]]; then diff --git a/actions/release-post-merge/action.yml b/actions/release-post-merge/action.yml index af2c1eb..1f31e68 100644 --- a/actions/release-post-merge/action.yml +++ b/actions/release-post-merge/action.yml @@ -49,6 +49,27 @@ outputs: runs: using: composite steps: + - name: Validate GitHub App credentials + shell: bash + env: + RELEASE_APP_ID: ${{ inputs.app-id }} + RELEASE_APP_PRIVATE_KEY: ${{ inputs.app-private-key }} + run: | + set -euo pipefail + + if [[ -z "${RELEASE_APP_ID}" ]]; then + echo "::error::GitHub App id is empty. Configure LIBRECODE_WORKFLOW_APP_ID as an Actions secret in the consumer repository or organization." + exit 1 + fi + if [[ ! "${RELEASE_APP_ID}" =~ ^[0-9]+$ ]]; then + echo "::error::GitHub App id must be numeric." + exit 1 + fi + if [[ -z "${RELEASE_APP_PRIVATE_KEY}" ]]; then + echo "::error::GitHub App private key is empty. Configure LIBRECODE_WORKFLOW_APP_PRIVATE_KEY as an Actions secret in the consumer repository or organization." + exit 1 + fi + - id: restore name: Restore preparation contracts uses: $/actions/release-artifact-restore diff --git a/actions/release-prepare/action.yml b/actions/release-prepare/action.yml index 9a99a61..b38b9a8 100644 --- a/actions/release-prepare/action.yml +++ b/actions/release-prepare/action.yml @@ -73,6 +73,27 @@ outputs: runs: using: composite steps: + - name: Validate GitHub App credentials + shell: bash + env: + RELEASE_APP_ID: ${{ inputs.app-id }} + RELEASE_APP_PRIVATE_KEY: ${{ inputs.app-private-key }} + run: | + set -euo pipefail + + if [[ -z "${RELEASE_APP_ID}" ]]; then + echo "::error::GitHub App id is empty. Configure LIBRECODE_WORKFLOW_APP_ID as an Actions secret in the consumer repository or organization." + exit 1 + fi + if [[ ! "${RELEASE_APP_ID}" =~ ^[0-9]+$ ]]; then + echo "::error::GitHub App id must be numeric." + exit 1 + fi + if [[ -z "${RELEASE_APP_PRIVATE_KEY}" ]]; then + echo "::error::GitHub App private key is empty. Configure LIBRECODE_WORKFLOW_APP_PRIVATE_KEY as an Actions secret in the consumer repository or organization." + exit 1 + fi + - id: plan name: Build release plan uses: $/actions/release-plan diff --git a/docs/cross-repository-automation.md b/docs/cross-repository-automation.md index 3d94fbc..e9a75e2 100644 --- a/docs/cross-repository-automation.md +++ b/docs/cross-repository-automation.md @@ -40,10 +40,22 @@ permissions are required. `LibreCodeCoop/github-workflows` stores: -- Actions variable `LIBRECODE_WORKFLOW_APP_ID`; +- Actions secret `LIBRECODE_WORKFLOW_APP_ID`; - Actions secret `LIBRECODE_WORKFLOW_APP_PRIVATE_KEY`. -The private key must never be committed to the repository. +Consumer repositories that execute write-capable release orchestration also need +both secrets available in their own Actions context, either directly at +repository level or inherited from an organization configuration that includes +the repository: + +- Actions secret `LIBRECODE_WORKFLOW_APP_ID`; +- Actions secret `LIBRECODE_WORKFLOW_APP_PRIVATE_KEY`. + +The GitHub App installation must also include the consumer repository. A +credential configured only in `LibreCodeCoop/github-workflows` is not visible +to a workflow running in another repository. + +The private key must never be committed to a repository. ## Token model diff --git a/tests/test_release_authorization.py b/tests/test_release_authorization.py index 0a2868d..4691627 100644 --- a/tests/test_release_authorization.py +++ b/tests/test_release_authorization.py @@ -33,6 +33,13 @@ def test_rejects_unknown_actual_permission(self) -> None: with self.assertRaisesRegex(ValueError, "unsupported repository permission"): module.is_authorized("custom", "read") + def test_action_summary_uses_printf_for_markdown_code(self) -> None: + content = (ROOT / "actions" / "release-authorization" / "action.yml").read_text(encoding="utf-8") + self.assertIn("printf -- '- Actor: `%s`", content) + self.assertIn("printf -- '- Required: `%s`", content) + self.assertIn("printf -- '- Actual: `%s`", content) + self.assertNotIn('echo "- Actor: `', content) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_release_post_merge_action.py b/tests/test_release_post_merge_action.py index 79aa9c6..a80315f 100644 --- a/tests/test_release_post_merge_action.py +++ b/tests/test_release_post_merge_action.py @@ -10,6 +10,14 @@ class ReleasePostMergeActionTest(unittest.TestCase): + def test_credentials_are_validated_before_artifact_restore(self) -> None: + content = ACTION.read_text(encoding="utf-8") + validate = content.index("Validate GitHub App credentials") + restore = content.index("Restore preparation contracts") + self.assertLess(validate, restore) + self.assertIn("LIBRECODE_WORKFLOW_APP_ID", content) + self.assertIn("LIBRECODE_WORKFLOW_APP_PRIVATE_KEY", content) + def test_authorization_happens_before_mutation(self) -> None: content = ACTION.read_text(encoding="utf-8") authorize = content.index("Authorize release PR merger") diff --git a/tests/test_release_prepare_action.py b/tests/test_release_prepare_action.py index 6982cd6..ace172d 100644 --- a/tests/test_release_prepare_action.py +++ b/tests/test_release_prepare_action.py @@ -28,6 +28,15 @@ def test_prepare_composes_policy_contracts_and_scoped_mutation(self) -> None: self.assertNotIn("permission-workflows: write", content) self.assertIn("release:prepare", content) + def test_prepare_validates_mutation_credentials_before_planning(self) -> None: + content = ACTION.read_text(encoding="utf-8") + validate = content.index("Validate GitHub App credentials") + plan = content.index("Build release plan") + self.assertLess(validate, plan) + self.assertIn("LIBRECODE_WORKFLOW_APP_ID", content) + self.assertIn("LIBRECODE_WORKFLOW_APP_PRIVATE_KEY", content) + self.assertIn('[[ ! "${RELEASE_APP_ID}" =~ ^[0-9]+$ ]]', content) + def test_prepare_persists_plan_and_preparation_by_pr_number(self) -> None: content = ACTION.read_text(encoding="utf-8") self.assertIn("release-plan.json", content)