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
6 changes: 6 additions & 0 deletions .github/workflows/reusable_release_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ jobs:
echo "version=$version"
} >> "$GITHUB_OUTPUT"

# Self-hosted runner images do not always carry one.
- name: Ensure an SSH client is available
run: |
command -v ssh >/dev/null && command -v scp >/dev/null && exit 0
sudo apt-get update -qq && sudo apt-get install -y -qq openssh-client

- name: Configure SSH to the publish host
env:
SSH_KEY: ${{ secrets.PUBLISH_SSH_KEY }}
Expand Down
99 changes: 99 additions & 0 deletions .github/workflows/reusable_release_tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ name: release tag
# distro in ros_distros on amd64 and arm64. A distro or architecture that does not build is a
# warning and uploads nothing; a released package without a root is not built, also a warning.
#
# What built is handed to a private publisher repository, which publishes it from a self-hosted
# runner: self-hosted runners never take jobs from public repositories. The caller passes
# secrets: inherit so RELEASE_APP_ID and RELEASE_APP_PRIVATE_KEY reach the dispatch.
#
# The calling job must grant contents: write; a called workflow cannot raise the token above what
# its caller holds. Requires a squash- or merge-committed pull request: a rebase merge's first
# parent is not pre-merge main, so nothing is found to tag.
Expand All @@ -31,6 +35,11 @@ on:
default: 'ubuntu-latest'
required: false
type: string
publisher:
description: 'Repository in this organization that publishes what was built; empty takes the RELEASE_PUBLISHER organization variable'
default: ''
required: false
type: string
ros_distros:
description: 'JSON list of the ROS distros to build the released packages for'
default: '["jazzy", "kilted", "lyrical", "rolling"]'
Expand Down Expand Up @@ -165,3 +174,93 @@ jobs:
ros_distro: ${{ matrix.ros_distro }}
runner: ${{ inputs.runner }}
continue_on_failure: true

publish:
needs: [tag, build]
if: ${{ !cancelled() && needs.tag.outputs.build != '[]' }}
runs-on: ${{ inputs.runner }}
# Reads this run's artifacts with the App: the caller grants contents: write only.
permissions: {}
steps:
- name: Check a publisher and the release App are available
id: app
env:
APP_ID: ${{ secrets.RELEASE_APP_ID }}
PUBLISHER: ${{ inputs.publisher || vars.RELEASE_PUBLISHER }}
run: |
if [ -z "$PUBLISHER" ]; then
echo "::warning::not published: no publisher repository named; set the RELEASE_PUBLISHER organization variable"
echo "ok=false" >> "$GITHUB_OUTPUT"
elif [ -z "$APP_ID" ]; then
echo "::warning::not published: RELEASE_APP_ID does not reach this workflow; give the tag job secrets: inherit"
echo "ok=false" >> "$GITHUB_OUTPUT"
else
echo "ok=true" >> "$GITHUB_OUTPUT"
echo "publisher=$PUBLISHER" >> "$GITHUB_OUTPUT"
fi

- uses: actions/create-github-app-token@v2
if: steps.app.outputs.ok == 'true'
id: token
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ steps.app.outputs.publisher }}
permission-contents: write

- uses: actions/create-github-app-token@v2
if: steps.app.outputs.ok == 'true'
id: artifacts
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
permission-actions: read

- name: Hand what built to the publisher
if: steps.app.outputs.ok == 'true'
env:
GH_TOKEN: ${{ steps.artifacts.outputs.token }}
DISPATCH_TOKEN: ${{ steps.token.outputs.token }}
BUILD: ${{ needs.tag.outputs.build }}
DISTROS: ${{ inputs.ros_distros }}
PUBLISHER: ${{ github.repository_owner }}/${{ steps.app.outputs.publisher }}
run: |
python3 - <<'PY'
import json, os, urllib.request
api, repo, run = os.environ["GITHUB_API_URL"], os.environ["GITHUB_REPOSITORY"], os.environ["GITHUB_RUN_ID"]

def call(url, token, body=None):
req = urllib.request.Request(url, data=body, headers={
"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"})
with urllib.request.urlopen(req) as r:
return json.loads(r.read() or b"null")

built, page = set(), 1
while True:
got = call(f"{api}/repos/{repo}/actions/runs/{run}/artifacts?per_page=100&page={page}",
os.environ["GH_TOKEN"])["artifacts"]
built |= {a["name"] for a in got}
if len(got) < 100:
break
page += 1

# Named as reusable_release_build.yml names them; a leg that failed uploaded nothing.
artifacts = []
for release in json.loads(os.environ["BUILD"]):
for distro in json.loads(os.environ["DISTROS"]):
for arch in ("amd64", "arm64"):
name = f'{release["package"]}-{release["tag"].replace("/", "-")}-{distro}-{arch}'
if name in built:
artifacts.append({"artifact": name, **release, "distro": distro, "arch": arch})
if not artifacts:
print("::warning::nothing built, so nothing was handed to the publisher")
raise SystemExit(0)
payload = {"repository": repo, "run_id": run, "artifacts": artifacts}
call(f'{api}/repos/{os.environ["PUBLISHER"]}/dispatches', os.environ["DISPATCH_TOKEN"],
json.dumps({"event_type": "publish", "client_payload": payload}).encode())
for a in artifacts:
print(f'handed {a["artifact"]} to {os.environ["PUBLISHER"]}')
PY
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,14 @@ merge-committed; after a rebase merge there is nothing for it to find. It then b
of every released package that declares `<duatic_archive_root>`, with `reusable_release_build.yml`,
for each distro in `ros_distros` (default jazzy, kilted, lyrical and rolling) on amd64 and
arm64. A distro or architecture that does not build is a warning on the run and uploads nothing; a
released package without a root is not built, also with a warning. Its `released` output lists
released package without a root is not built, also with a warning. What built is then handed, with a
`repository_dispatch`, to the private publisher repository the organization variable
`RELEASE_PUBLISHER` names (or the `publisher` input), which publishes it into the apt archive from a
self-hosted runner: self-hosted runners never take jobs from public repositories. The tag job passes
`secrets: inherit` so that `RELEASE_APP_ID` and `RELEASE_APP_PRIVATE_KEY` reach the dispatch; without
them, or with no publisher named, the run warns that nothing was published. The App reads the run's
artifacts, which needs Actions read on the repository, and dispatches, which needs Contents write
on the publisher. Its `released` output lists
`{package, version, tag, root}` for what it tagged, `[]` otherwise.

**`reusable_repo_release_tag.yml`** runs when a repo-wide release pull request merges. Every
Expand Down
Loading