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.21
0.6.22
58 changes: 48 additions & 10 deletions actions/release-prepare/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 8 additions & 2 deletions tests/test_release_prepare_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading