Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.1
0.6.2
8 changes: 4 additions & 4 deletions actions/release-authorization/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions actions/release-post-merge/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions actions/release-prepare/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions docs/cross-repository-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions tests/test_release_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
8 changes: 8 additions & 0 deletions tests/test_release_post_merge_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
9 changes: 9 additions & 0 deletions tests/test_release_prepare_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading