Skip to content
Open
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
23 changes: 20 additions & 3 deletions .github/ci/publish_oakapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ OAKCTL_HUB_TOKEN="your_token_here" \

Optional environment variables:

- `LUXONIS_OFFICIAL=true` to replace `com.example` with `com.luxonis`
on the `identifier` line. Defaults to `true`.
- `LUXONIS_OFFICIAL_IDENTIFIER=true` to replace
`com.example.<top-level-folder>` with `com.luxonis` on the `identifier`
line. Defaults to `true`.
- `NEW_IDENTIFIER="com.luxonis.myapp"` to override the `identifier`
line (takes precedence over `LUXONIS_OFFICIAL`).
line (takes precedence over `LUXONIS_OFFICIAL_IDENTIFIER`).

Notes:

Expand All @@ -30,3 +31,19 @@ Notes:
file on exit, even if the run fails.
- This performs a real publish; consider using a temp copy of an example
if you want to avoid touching your working tree.

## Bulk publishing

The `Publish OAK Apps` workflow publishes the curated list in
`.github/publish_oakapps.txt`. Run it from GitHub Actions with an optional
comma-separated `exclude_apps` input. Set `dry_run` to validate the list and
show the identifier plan without reserving a testbed or publishing.

For example, from the repository root:

```bash
gh workflow run publish_oakapps.yaml \
--repo luxonis/oak-examples \
--ref main \
-f exclude_apps=apps/dino-tracking,neural-networks/ocr/general-ocr
```
35 changes: 31 additions & 4 deletions .github/ci/publish_oakapp/publish_oakapp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,19 @@ fi

cd "$ROOT_DIR"

ROOT_DIR_FOR_IDENTIFIER="${ROOT_DIR#./}"
TOP_LEVEL_FOLDER="${ROOT_DIR_FOR_IDENTIFIER%%/*}"

OAKAPP_TOML="oakapp.toml"
OAKAPP_BACKUP="$(mktemp)"
OAKAPP_FILE=""
OAKAPP_EDITED_TMP="${OAKAPP_TOML}.tmp"

replace_identifier() {
local target_identifier="$1"
sed -E "s|^[[:space:]]*identifier[[:space:]]*=.*|identifier = \"${target_identifier}\"|" "$OAKAPP_TOML" > "$OAKAPP_EDITED_TMP"
mv "$OAKAPP_EDITED_TMP" "$OAKAPP_TOML"
}

cleanup() {
set +e
Expand All @@ -42,24 +52,41 @@ cleanup() {
if [[ -n "$OAKAPP_FILE" && -f "$OAKAPP_FILE" ]]; then
rm -f "$OAKAPP_FILE"
fi
if [[ -f "$OAKAPP_EDITED_TMP" ]]; then
rm -f "$OAKAPP_EDITED_TMP"
fi
}

trap cleanup EXIT

cp "$OAKAPP_TOML" "$OAKAPP_BACKUP"

if [[ -n "$NEW_IDENTIFIER" ]]; then
if ! grep -qE '^identifier\s*=' "$OAKAPP_TOML"; then
if [[ ! "$NEW_IDENTIFIER" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
echo "Invalid identifier: $NEW_IDENTIFIER" >&2
exit 1
fi
if ! grep -qE '^identifier[[:space:]]*=' "$OAKAPP_TOML"; then
echo "identifier not found in $OAKAPP_TOML" >&2
exit 1
fi
sed -i -E "s/^identifier\\s*=.*/identifier = \"${NEW_IDENTIFIER}\"/" "$OAKAPP_TOML"
replace_identifier "$NEW_IDENTIFIER"
elif [[ "$LUXONIS_OFFICIAL_IDENTIFIER" == "true" ]]; then
if ! grep -qE '^identifier\s*=' "$OAKAPP_TOML"; then
if ! grep -qE '^identifier[[:space:]]*=' "$OAKAPP_TOML"; then
echo "identifier not found in $OAKAPP_TOML" >&2
exit 1
fi
sed -i -E '/^identifier\s*=/{s/com\.example/com.luxonis/}' "$OAKAPP_TOML"
CURRENT_IDENTIFIER=$(sed -n -E 's/^[[:space:]]*identifier[[:space:]]*=[[:space:]]*"([^"]*)".*$/\1/p' "$OAKAPP_TOML" | head -n 1)
IDENTIFIER_PREFIX="com.example.${TOP_LEVEL_FOLDER}."
if [[ "$CURRENT_IDENTIFIER" == com.luxonis.* ]]; then
echo "Identifier is already in the com.luxonis namespace; leaving it unchanged: ${CURRENT_IDENTIFIER}"
elif [[ "$CURRENT_IDENTIFIER" == "$IDENTIFIER_PREFIX"* ]]; then
TARGET_IDENTIFIER="com.luxonis.${CURRENT_IDENTIFIER#"$IDENTIFIER_PREFIX"}"
replace_identifier "$TARGET_IDENTIFIER"
else
echo "Identifier does not use the expected '${IDENTIFIER_PREFIX}' prefix: ${CURRENT_IDENTIFIER}" >&2
exit 1
fi
fi

if ! command -v oakctl >/dev/null 2>&1; then
Expand Down
16 changes: 16 additions & 0 deletions .github/publish_oakapps.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Curated set of OAK Apps published by the bulk release workflow.
# Paths are relative to the repository root. Keep one app per line.
apps/object-volume-measurement-3d
apps/people-demographics-and-sentiment-analysis
apps/qr-tiling
apps/dino-tracking
apps/data-collection
apps/focused-vision
apps/p2p-measurement
custom-frontend/open-vocabulary-object-detection
camera-controls/lossless-zooming
neural-networks/ocr/general-ocr
neural-networks/face-detection/age-gender
neural-networks/segmentation/depth-crop
neural-networks/pose-estimation/hand-pose
neural-networks/object-detection/spatial-detections
2 changes: 1 addition & 1 deletion .github/workflows/publish_oakapp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
type: string
required: true
new_identifier:
description: "Override whole identifier value in oakapp.toml. By default only `com.example` will be replaced by `com.luxonis`"
description: "Override identifier. Otherwise com.example.<top-level-folder> becomes com.luxonis"
type: string
required: false

Expand Down
194 changes: 194 additions & 0 deletions .github/workflows/publish_oakapps.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
name: Publish OAK Apps

on:
workflow_dispatch:
inputs:
exclude_apps:
description: "Comma-separated app paths to exclude from the curated publish list."
type: string
required: false
dry_run:
description: "Validate and show the publish plan without reserving a testbed or publishing."
type: boolean
default: false
required: false

concurrency:
group: publish-oakapps
cancel-in-progress: false

permissions:
contents: read

jobs:
select:
name: Select apps
runs-on: ubuntu-latest
outputs:
apps: ${{ steps.select.outputs.apps }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Resolve publish list
id: select
env:
EXCLUDE_APPS: ${{ inputs.exclude_apps }}
run: |
set -euo pipefail

manifest=".github/publish_oakapps.txt"
if [[ ! -f "$manifest" ]]; then
echo "Publish manifest not found: $manifest" >&2
exit 1
fi

mapfile -t manifest_apps < <(
sed -E 's/[[:space:]]*#.*$//' "$manifest" |
sed -E 's/^[[:space:]]+|[[:space:]]+$//g' |
sed '/^$/d'
)

if [[ "${#manifest_apps[@]}" -eq 0 ]]; then
echo "Publish manifest is empty." >&2
exit 1
fi

contains_app() {
local needle="$1"
local candidate
for candidate in "${manifest_apps[@]}"; do
if [[ "$candidate" == "$needle" ]]; then
return 0
fi
done
return 1
}

declare -A excluded=()
if [[ -n "${EXCLUDE_APPS:-}" ]]; then
IFS=',' read -ra requested_exclusions <<< "$EXCLUDE_APPS"
for raw_app in "${requested_exclusions[@]}"; do
app=$(printf '%s' "$raw_app" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
if [[ -z "$app" ]]; then
echo "Empty app path in exclude_apps: '$EXCLUDE_APPS'" >&2
exit 1
fi
if ! contains_app "$app"; then
echo "Excluded app is not in the publish manifest: $app" >&2
exit 1
fi
if [[ -n "${excluded[$app]+x}" ]]; then
echo "Duplicate app in exclude_apps: $app" >&2
exit 1
fi
excluded["$app"]=1
done
fi

declare -A target_identifiers=()
selected_apps=()
for app in "${manifest_apps[@]}"; do
if [[ -n "${excluded[$app]+x}" ]]; then
continue
fi
if [[ ! -d "$app" || ! -f "$app/oakapp.toml" ]]; then
echo "Manifest app is missing its directory or oakapp.toml: $app" >&2
exit 1
fi
selected_apps+=("$app")

identifier=$(sed -n -E 's/^[[:space:]]*identifier[[:space:]]*=[[:space:]]*"([^"]*)".*/\1/p' "$app/oakapp.toml" | head -n 1)
top_level="${app%%/*}"
prefix="com.example.${top_level}."
if [[ "$identifier" != "$prefix"* ]]; then
echo "App identifier does not use the expected '${prefix}' prefix: $app ($identifier)" >&2
exit 1
fi
target_identifier="com.luxonis.${identifier#"$prefix"}"
if [[ -n "${target_identifiers[$target_identifier]+x}" ]]; then
echo "Duplicate target identifier '${target_identifier}' for app: $app" >&2
exit 1
fi
target_identifiers["$target_identifier"]="$app"
done

if [[ "${#selected_apps[@]}" -eq 0 ]]; then
echo "No apps remain after applying exclude_apps." >&2
exit 1
fi

echo "Publish plan:"
for app in "${selected_apps[@]}"; do
identifier=$(sed -n -E 's/^[[:space:]]*identifier[[:space:]]*=[[:space:]]*"([^"]*)".*/\1/p' "$app/oakapp.toml" | head -n 1)
app_version=$(sed -n -E 's/^[[:space:]]*app_version[[:space:]]*=[[:space:]]*"([^"]*)".*/\1/p' "$app/oakapp.toml" | head -n 1)
if [[ -z "$app_version" ]]; then
echo "App version is missing: $app" >&2
exit 1
fi
top_level="${app%%/*}"
prefix="com.example.${top_level}."
target_identifier="com.luxonis.${identifier#"$prefix"}"
printf ' %s (version %s): %s -> %s\n' "$app" "$app_version" "$identifier" "$target_identifier"
done

matrix_json=$(printf '%s\n' "${selected_apps[@]}" | jq -Rsc 'split("\n") | map(select(length > 0))')
echo "apps=$matrix_json" >> "$GITHUB_OUTPUT"

{
echo "### OAK App publish plan"
echo
echo '```text'
printf '%s\n' "${selected_apps[@]}"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

publish:
name: Publish ${{ matrix.app }}
needs: select
if: ${{ inputs.dry_run == false }}
runs-on: ['self-hosted', 'testbed-runner']
strategy:
fail-fast: false
max-parallel: 3
matrix:
app: ${{ fromJSON(needs.select.outputs.apps) }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
cache: pip

- name: Install HIL
run: |
pip install hil_framework --upgrade --index-url https://__token__:${{ secrets.GITLAB_TOKEN }}@gitlab.luxonis.com/api/v4/projects/213/packages/pypi/simple

- name: Publish app via HIL runner
env:
ROOT_DIR: ${{ matrix.app }}
OAKCTL_HUB_TOKEN: ${{ secrets.OAKCTL_HUB_TOKEN }}
run: |
set -euo pipefail
app_slug="${ROOT_DIR//\//-}"
reservation_name="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}#publish-oakapp-${app_slug}"

ENV_VARS="\
--env ROOT_DIR=$ROOT_DIR \
--env OAKCTL_HUB_TOKEN=$OAKCTL_HUB_TOKEN"

exec hil_runner --models "oak4_pro or oak4_d" --reservation-name "$reservation_name" --wait --oakctl --sync-workspace \
--dockerfile ./.github/ci/publish_oakapp/Dockerfile \
--docker-run-args "$ENV_VARS"

- name: Record result
if: ${{ always() }}
run: |
{
echo "### ${{ matrix.app }}"
echo
echo "Result: ${{ job.status }}"
} >> "$GITHUB_STEP_SUMMARY"
Loading
Loading