diff --git a/VERSION b/VERSION index 33ac520..d15dfad 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.21 +0.6.22 diff --git a/actions/release-prepare/action.yml b/actions/release-prepare/action.yml index 7e1b335..c805516 100644 --- a/actions/release-prepare/action.yml +++ b/actions/release-prepare/action.yml @@ -39,8 +39,13 @@ inputs: description: Read-only caller token used for planning and permission lookup. required: true app-id: - description: GitHub App id used for the short-lived mutation token. - required: true + description: Legacy GitHub App id retained for consumer compatibility. + required: false + default: '' + app-slug: + description: Public GitHub App slug used to resolve its client id. + required: false + default: librecode-workflow-automation app-private-key: description: GitHub App private key used for the short-lived mutation token. required: true @@ -68,17 +73,13 @@ runs: - name: Validate GitHub App credentials shell: bash env: - RELEASE_APP_ID: ${{ inputs.app-id }} + RELEASE_APP_SLUG: ${{ inputs.app-slug }} 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 variable in the consumer repository or organization." - exit 1 - fi - if [[ ! "${RELEASE_APP_ID}" =~ ^[0-9]+$ ]]; then - echo "::error::GitHub App id must be numeric." + if [[ -z "${RELEASE_APP_SLUG}" ]]; then + echo "::error::GitHub App slug is empty." exit 1 fi if [[ -z "${RELEASE_APP_PRIVATE_KEY}" ]]; then @@ -130,11 +131,29 @@ runs: echo "owner=${RELEASE_REPOSITORY%%/*}" >> "${GITHUB_OUTPUT}" echo "name=${RELEASE_REPOSITORY#*/}" >> "${GITHUB_OUTPUT}" + - id: app-identity + name: Resolve GitHub App client id + shell: bash + env: + RELEASE_APP_SLUG: ${{ inputs.app-slug }} + RELEASE_GITHUB_TOKEN: ${{ inputs.github-token }} + RELEASE_GITHUB_API_URL: ${{ github.api_url }} + run: | + set -euo pipefail + + app_json="$(curl --fail --silent --show-error --location -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${RELEASE_GITHUB_TOKEN}" -H "X-GitHub-Api-Version: 2022-11-28" "${RELEASE_GITHUB_API_URL}/apps/${RELEASE_APP_SLUG}")" + client_id="$(php -r '$d=json_decode(stream_get_contents(STDIN),true,512,JSON_THROW_ON_ERROR); echo $d["client_id"] ?? "";' <<< "${app_json}")" + if [[ -z "${client_id}" ]]; then + echo "::error::GitHub App metadata did not contain a client_id for ${RELEASE_APP_SLUG}." + exit 1 + fi + echo "client-id=${client_id}" >> "${GITHUB_OUTPUT}" + - id: app-token name: Create scoped GitHub App token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ inputs.app-id }} + client-id: ${{ steps.app-identity.outputs.client-id }} private-key: ${{ inputs.app-private-key }} owner: ${{ steps.repository.outputs.owner }} repositories: ${{ steps.repository.outputs.name }} @@ -182,6 +201,25 @@ runs: printf -- '- State artifact: `%s`\n' "${artifact_name}" } >> "${GITHUB_STEP_SUMMARY}" + - name: Apply release pull request metadata + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + RELEASE_REPOSITORY: ${{ github.repository }} + RELEASE_ACTOR: ${{ inputs.actor }} + RELEASE_PLAN_PATH: ${{ steps.plan.outputs.plan-path }} + RELEASE_PR_NUMBER: ${{ steps.prepare.outputs.pull-request-number }} + run: | + set -euo pipefail + + milestone_number="$(php -r '$p=json_decode(file_get_contents($argv[1]),true,512,JSON_THROW_ON_ERROR); echo $p["milestone"]["number"] ?? "";' "${RELEASE_PLAN_PATH}")" + if [[ -z "${milestone_number}" ]]; then + echo "::error::ReleasePlan does not contain a milestone number." + exit 1 + fi + payload="$(php -r 'echo json_encode(["assignees"=>[$argv[1]],"milestone"=>(int)$argv[2]],JSON_THROW_ON_ERROR);' "${RELEASE_ACTOR}" "${milestone_number}")" + printf '%s' "${payload}" | gh api --method PATCH "repos/${RELEASE_REPOSITORY}/issues/${RELEASE_PR_NUMBER}" --input - >/dev/null + - name: Persist release preparation contracts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/tests/test_release_prepare_action.py b/tests/test_release_prepare_action.py index b297180..2bcb8ed 100644 --- a/tests/test_release_prepare_action.py +++ b/tests/test_release_prepare_action.py @@ -26,16 +26,22 @@ def test_prepare_composes_policy_contracts_and_scoped_mutation(self) -> None: self.assertIn("permission-contents: write", content) self.assertIn("permission-pull-requests: write", content) self.assertNotIn("permission-workflows: write", content) + self.assertIn("client-id: ${{ steps.app-identity.outputs.client-id }}", content) + self.assertNotIn("app-id: ${{ inputs.app-id }}", content) self.assertIn("release:prepare", content) + self.assertIn("Apply release pull request metadata", content) + self.assertIn('"assignees"=>[$argv[1]]', content) + self.assertIn('"milestone"=>(int)$argv[2]', 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) + self.assertIn("librecode-workflow-automation", content) + self.assertIn("/apps/${RELEASE_APP_SLUG}", content) + self.assertIn('"client_id"', content) def test_prepare_persists_plan_and_preparation_by_pr_number(self) -> None: content = ACTION.read_text(encoding="utf-8")