Merge branch 'test/dolomite-plugin' into 'main' #357
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # plugin-publish.yml — GitHub Actions (publish-only) | |
| # | |
| # Scope (intentionally narrow): | |
| # 1. Detect plugins changed in the latest push to main | |
| # 2. Compile each plugin for 9 targets (3 macOS, 4 Linux musl, 3 Windows MSVC) | |
| # — plugin × target matrix runs every (plugin, platform) cell in parallel | |
| # 3. Create GitHub Release per plugin (tag: plugins/<name>@<version>) | |
| # 4. Upload binaries + checksums.txt as release assets | |
| # | |
| # Out of scope (handled by GitLab on-prem): | |
| # × lint / static security rules | |
| # × build verification (we trust GitLab's build check) | |
| # × AI code review | |
| # × pre-flight injection into SKILL.md | |
| # × registry.json / marketplace.json regeneration | |
| # × user-facing PR feedback | |
| # | |
| # Trust boundary (relaxed mode — see verify-source job): | |
| # GitHub repo accepts NO direct push from any human, NO pull_request, and | |
| # NO pull_request_target. The ONLY ingress is the GitLab sync bot pushing | |
| # to `main` via deploy key. Since there is no inbound surface, verify-source | |
| # currently records the actor for audit but does NOT block. Re-tighten | |
| # later once a concrete bot actor name is observed. | |
| # | |
| # Triggers: | |
| # • push: branches=[main] from GitLab mirror sync only (no other ingress exists) | |
| # | |
| # Auditable design choices: | |
| # - Top-level permissions: read-all. Only create-release opts into contents: write. | |
| # - All actions pinned by SHA. | |
| # - Strict input validation in detect (semver / SHA40 / shell-safe names / path traversal refusal). | |
| # - Releases are append-only and immutable: skip if tag already exists. | |
| # - notify-failure runs on any job failure so silent CI breakage is detected. | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| name: "Publish — compile, tag, release" | |
| on: | |
| push: | |
| branches: [main] | |
| # Only fire when something publish-relevant changes | |
| paths: | |
| - 'skills/**' | |
| - 'registry.json' | |
| - '.claude-plugin/marketplace.json' | |
| # Top-level: read-only. Each job opts into the minimum needed (e.g. contents: write). | |
| permissions: read-all | |
| # Releases are append-only and immutable; do not cancel an in-flight publish. | |
| concurrency: | |
| group: publish-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # JOB 1 verify-source (RELAXED — accept all pushes for now) | |
| # | |
| # GitHub repo accepts no inbound push other than the GitLab mirror sync | |
| # (deploy key auth). There is no PR / pull_request_target / human-push | |
| # surface. We log the actor for future audit / tightening but do not | |
| # block. When we have observed a stable actor identity, restore the | |
| # `if [ "$ACTOR" != "$BOT" ]; then exit 1; fi` check. | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| verify-source: | |
| name: Verify source (relaxed) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| verified: ${{ steps.check.outputs.verified }} | |
| steps: | |
| - id: check | |
| env: | |
| EVENT: ${{ github.event_name }} | |
| ACTOR: ${{ github.actor }} | |
| run: | | |
| # RELAXED mode: log only, do not block. | |
| # GitHub repo has no inbound push surface other than the GitLab | |
| # mirror sync (deploy key auth). Any commit on main therefore | |
| # came from gitlab.okg.com/mobilex/web3/OKPluginStore. The | |
| # actor name observed here is the GitHub side's interpretation | |
| # of the deploy-key push — record it so future audit can | |
| # restore strict actor matching. | |
| echo "push by actor='${ACTOR}' event='${EVENT}' — accepted (relaxed mode)" | |
| echo "verified=true" >> "$GITHUB_OUTPUT" | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # JOB 2 detect | |
| # | |
| # Find which plugins changed in this push. A plugin is "buildable" if | |
| # skills/<name>/plugin.yaml contains a `build:` section with lang in | |
| # {rust, go}. Strict validation rejects anything unsafe for shell args. | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| detect: | |
| name: Detect changes | |
| needs: verify-source | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| has_builds: ${{ steps.build_info.outputs.has_builds }} | |
| build_plugins_json: ${{ steps.build_info.outputs.build_plugins_json }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Install yq (pinned + SHA256 verified) | |
| run: | | |
| # Pin yq to a specific tag and SHA256. If GitHub mirror is | |
| # tampered with or the asset is replaced, the sha256sum -c step | |
| # below refuses to use it. The expected SHA is taken from yq's | |
| # own published checksums.txt in the same GitHub release. | |
| set -euo pipefail | |
| YQ_VERSION=v4.44.3 | |
| # SHA256 of yq_linux_amd64 for v4.44.3 (from the release's checksums.txt). | |
| YQ_SHA256=eef0fb0da6bdaee52b22a9adf6dc7ddb7df73d24d5c0e94c69aabfd76d4dca7f | |
| TMP=$(mktemp) | |
| curl -sSL -o "${TMP}" \ | |
| "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" | |
| echo "${YQ_SHA256} ${TMP}" | sha256sum -c - | |
| sudo mv "${TMP}" /usr/local/bin/yq | |
| sudo chmod +x /usr/local/bin/yq | |
| yq --version | |
| - name: List changed plugins | |
| id: changed | |
| env: | |
| BEFORE: ${{ github.event.before }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then | |
| # First push to main / branch was just created. Build everything. | |
| PLUGINS=$(ls -d skills/*/plugin.yaml 2>/dev/null \ | |
| | xargs -I{} dirname {} | xargs -I{} basename {} \ | |
| | sort -u | tr '\n' ' ' | sed 's/ $//') | |
| echo "first-push mode: ${PLUGINS}" | |
| else | |
| # Normal sync: diff before…HEAD, take skills/<name> level | |
| git fetch --depth=50 origin "$BEFORE" 2>/dev/null || true | |
| PLUGINS=$(git diff --name-only "$BEFORE...HEAD" -- 'skills/' \ | |
| | cut -d'/' -f2 | sort -u | grep -v '^$' \ | |
| | tr '\n' ' ' | sed 's/ $//') | |
| echo "diff mode: ${PLUGINS:-<none>}" | |
| fi | |
| if [ -z "$PLUGINS" ]; then | |
| echo "changed_plugins=" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "changed_plugins=${PLUGINS}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Filter to plugins with build section + collect metadata | |
| id: build_info | |
| env: | |
| PLUGINS: ${{ steps.changed.outputs.changed_plugins }} | |
| run: | | |
| set -euo pipefail | |
| BUILD_PLUGINS_JSON='[]' | |
| for p in $PLUGINS; do | |
| # Strict name validation — refuse anything we cannot safely use in shell args / URLs | |
| if ! echo "$p" | grep -qE '^[a-z0-9][a-z0-9-]*[a-z0-9]$'; then | |
| echo "::warning::skipping invalid plugin name '$p'" | |
| continue | |
| fi | |
| YAML="skills/${p}/plugin.yaml" | |
| [ -f "$YAML" ] || { echo "no plugin.yaml for $p, skip"; continue; } | |
| HAS_BUILD=$(yq '.build // null | type == "!!map"' "$YAML") | |
| [ "$HAS_BUILD" = "true" ] || { echo "$p has no build section, skip"; continue; } | |
| LANG=$(yq -r '.build.lang // ""' "$YAML") | |
| BIN=$(yq -r '.build.binary_name // ""' "$YAML") | |
| VER=$(yq -r '.version // ""' "$YAML") | |
| REPO=$(yq -r '.build.source_repo // ""' "$YAML") | |
| COMMIT=$(yq -r '.build.source_commit // ""' "$YAML") | |
| SUBDIR=$(yq -r '.build.source_dir // "."' "$YAML") | |
| # Validate every field we will later interpolate into shell | |
| echo "$VER" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+([-+][a-zA-Z0-9.-]+)?$' \ | |
| || { echo "::warning::$p version '$VER' not semver, skip"; continue; } | |
| echo "$BIN" | grep -qE '^[a-zA-Z0-9._-]+$' \ | |
| || { echo "::warning::$p binary_name '$BIN' invalid, skip"; continue; } | |
| case "$LANG" in | |
| rust|go) : ;; | |
| *) echo "::warning::$p lang '$LANG' not in {rust,go}, skip"; continue ;; | |
| esac | |
| if [ -n "$REPO" ] && [ "$REPO" != "null" ]; then | |
| echo "$REPO" | grep -qE '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' \ | |
| || { echo "::warning::$p source_repo '$REPO' invalid, skip"; continue; } | |
| fi | |
| if [ -n "$COMMIT" ] && [ "$COMMIT" != "null" ]; then | |
| echo "$COMMIT" | grep -qE '^[0-9a-f]{40}$' \ | |
| || { echo "::warning::$p source_commit '$COMMIT' not a 40-char hex SHA (branch / tag refused), skip"; continue; } | |
| fi | |
| case "$SUBDIR" in | |
| *..*|/*) | |
| echo "::warning::$p source_dir '$SUBDIR' contains '..' or is absolute, skip" | |
| continue | |
| ;; | |
| esac | |
| BUILD_PLUGINS_JSON=$(echo "$BUILD_PLUGINS_JSON" | jq \ | |
| --arg n "$p" --arg l "$LANG" --arg b "$BIN" --arg v "$VER" \ | |
| --arg r "$REPO" --arg c "$COMMIT" --arg s "$SUBDIR" \ | |
| '. += [{name:$n, lang:$l, binary_name:$b, version:$v, source_repo:$r, source_commit:$c, source_dir:$s}]') | |
| echo " + $p ($LANG) $BIN@$VER" | |
| done | |
| # GitHub Actions output: serialise JSON onto one line | |
| ONE_LINE=$(echo "$BUILD_PLUGINS_JSON" | jq -c .) | |
| echo "build_plugins_json=${ONE_LINE}" >> "$GITHUB_OUTPUT" | |
| COUNT=$(echo "$BUILD_PLUGINS_JSON" | jq 'length') | |
| [ "$COUNT" -gt 0 ] && echo "has_builds=true" >> "$GITHUB_OUTPUT" \ | |
| || echo "has_builds=false" >> "$GITHUB_OUTPUT" | |
| echo "buildable plugins: $COUNT" | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # JOB 3 build-release | |
| # | |
| # Build matrix — plugin × target. Every (plugin, platform) cell runs | |
| # in its own runner, in parallel up to max-parallel. Linux musl variants | |
| # are cross-compiled from Ubuntu via cargo-zigbuild; macOS / Windows use | |
| # GitHub-hosted native runners. fail-fast: false so one bad cell does | |
| # not cancel the rest. | |
| # | |
| # Matrix expansion: N plugins × 9 targets jobs per push. GitHub Actions | |
| # caps total matrix at 256, so single-push changes of more than 28 | |
| # buildable plugins will be rejected by the platform. In practice MR | |
| # diffs are 1-3 plugins, well within the cap. | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| build-release: | |
| name: Build ${{ matrix.plugin.name }} for ${{ matrix.target }} | |
| needs: detect | |
| if: needs.detect.outputs.has_builds == 'true' | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 50 | |
| matrix: | |
| plugin: ${{ fromJSON(needs.detect.outputs.build_plugins_json) }} | |
| target: | |
| - x86_64-apple-darwin | |
| - aarch64-apple-darwin | |
| - x86_64-unknown-linux-musl | |
| - i686-unknown-linux-musl | |
| - aarch64-unknown-linux-musl | |
| - armv7-unknown-linux-musleabihf | |
| - x86_64-pc-windows-msvc | |
| - i686-pc-windows-msvc | |
| - aarch64-pc-windows-msvc | |
| include: | |
| - target: x86_64-apple-darwin | |
| os: macos-latest | |
| - target: aarch64-apple-darwin | |
| os: macos-latest | |
| - target: x86_64-unknown-linux-musl | |
| os: ubuntu-latest | |
| zigbuild: true | |
| - target: i686-unknown-linux-musl | |
| os: ubuntu-latest | |
| zigbuild: true | |
| - target: aarch64-unknown-linux-musl | |
| os: ubuntu-latest | |
| zigbuild: true | |
| - target: armv7-unknown-linux-musleabihf | |
| os: ubuntu-latest | |
| zigbuild: true | |
| - target: x86_64-pc-windows-msvc | |
| os: windows-latest | |
| - target: i686-pc-windows-msvc | |
| os: windows-latest | |
| - target: aarch64-pc-windows-msvc | |
| os: windows-latest | |
| steps: | |
| - name: Set up Go (for govulncheck on go plugins) | |
| uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version: 'stable' | |
| check-latest: true | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Set up Zig (musl cross-compile) | |
| if: matrix.zigbuild | |
| uses: mlugg/setup-zig@53fc45b17fe98b52f92ee5ea08ff48a85a3e7eb7 # v1 | |
| with: | |
| version: 0.13.0 | |
| - name: Install cargo-zigbuild | |
| if: matrix.zigbuild | |
| run: cargo install cargo-zigbuild --locked | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Build ${{ matrix.plugin.name }} for ${{ matrix.target }} | |
| shell: bash | |
| env: | |
| NAME: ${{ matrix.plugin.name }} | |
| LANG: ${{ matrix.plugin.lang }} | |
| BIN: ${{ matrix.plugin.binary_name }} | |
| VER: ${{ matrix.plugin.version }} | |
| REPO: ${{ matrix.plugin.source_repo }} | |
| SHA: ${{ matrix.plugin.source_commit }} | |
| SUB: ${{ matrix.plugin.source_dir }} | |
| TARGET: ${{ matrix.target }} | |
| ZIGBUILD: ${{ matrix.zigbuild }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p artifacts | |
| EXT="" | |
| case "$TARGET" in *-pc-windows-msvc) EXT=".exe" ;; esac | |
| # ── Resolve source: local skills/<name>/ or external repo@commit | |
| SRC_ROOT="" | |
| if [ -n "$REPO" ] && [ "$REPO" != "null" ] && [ -n "$SHA" ] && [ "$SHA" != "null" ]; then | |
| # External repo: hard-pin to commit SHA, never branch / tag | |
| echo "::group::clone ${REPO}@${SHA:0:12}" | |
| EXT_DIR="$RUNNER_TEMP/src-$NAME" | |
| git clone --filter=blob:none --no-checkout "https://github.com/${REPO}.git" "$EXT_DIR" | |
| git -C "$EXT_DIR" fetch --depth 1 origin "$SHA" | |
| git -C "$EXT_DIR" checkout "$SHA" | |
| SRC_ROOT="$EXT_DIR/$SUB" | |
| echo "::endgroup::" | |
| else | |
| SRC_ROOT="skills/$NAME/$SUB" | |
| fi | |
| pushd "$SRC_ROOT" >/dev/null | |
| echo "::group::build $NAME ($LANG) for $TARGET" | |
| case "$LANG" in | |
| rust) | |
| if [ "$ZIGBUILD" = "true" ]; then | |
| cargo zigbuild --release --target "$TARGET" | |
| else | |
| cargo build --release --target "$TARGET" | |
| fi | |
| OUT_PATH="target/${TARGET}/release/${BIN}${EXT}" | |
| ;; | |
| go) | |
| if [ "$TARGET" = "x86_64-unknown-linux-musl" ]; then | |
| GOOS=linux GOARCH=amd64 | |
| elif [ "$TARGET" = "aarch64-unknown-linux-musl" ]; then | |
| GOOS=linux GOARCH=arm64 | |
| elif [ "$TARGET" = "i686-unknown-linux-musl" ]; then | |
| GOOS=linux GOARCH=386 | |
| elif [ "$TARGET" = "armv7-unknown-linux-musleabihf" ]; then | |
| GOOS=linux GOARCH=arm GOARM=7 | |
| elif [ "$TARGET" = "x86_64-apple-darwin" ]; then | |
| GOOS=darwin GOARCH=amd64 | |
| elif [ "$TARGET" = "aarch64-apple-darwin" ]; then | |
| GOOS=darwin GOARCH=arm64 | |
| elif [ "$TARGET" = "x86_64-pc-windows-msvc" ]; then | |
| GOOS=windows GOARCH=amd64 | |
| elif [ "$TARGET" = "i686-pc-windows-msvc" ]; then | |
| GOOS=windows GOARCH=386 | |
| elif [ "$TARGET" = "aarch64-pc-windows-msvc" ]; then | |
| GOOS=windows GOARCH=arm64 | |
| else | |
| echo "::error::unsupported target $TARGET for go" | |
| exit 1 | |
| fi | |
| export GOOS GOARCH GOARM CGO_ENABLED=0 | |
| go build -ldflags="-s -w" -o "${BIN}${EXT}" . | |
| OUT_PATH="${BIN}${EXT}" | |
| ;; | |
| *) echo "::error::lang $LANG not supported here"; exit 1 ;; | |
| esac | |
| echo "::endgroup::" | |
| # Stage as <bin>-<target>[.exe] for the release asset name | |
| ASSET="${BIN}-${TARGET}${EXT}" | |
| cp "$OUT_PATH" "${GITHUB_WORKSPACE}/artifacts/${ASSET}" | |
| ( cd "${GITHUB_WORKSPACE}/artifacts" && \ | |
| if command -v sha256sum >/dev/null; then sha256sum "$ASSET"; \ | |
| else shasum -a 256 "$ASSET"; fi ) | |
| popd >/dev/null | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| # Include plugin name so two plugins targeting the same platform | |
| # do not collide on artifact name. | |
| name: build-${{ matrix.plugin.name }}-${{ matrix.target }} | |
| path: artifacts/* | |
| if-no-files-found: error | |
| retention-days: 7 | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # JOB 4 create-release | |
| # | |
| # Per plugin in build_plugins_json: | |
| # • Tag: plugins/<name>@<version> | |
| # • Title: <name> <version> | |
| # • Body: short auto-generated notes (no commit-author leakage) | |
| # • Assets: every artifact produced by the matrix + checksums.txt | |
| # | |
| # If the tag already exists, we SKIP — releases are immutable. To re-publish, | |
| # bump plugin.yaml version in GitLab and resync. | |
| # | |
| # Note on `checksums.txt` naming: | |
| # Multiple plugins can share the artifacts/ directory after | |
| # merge-multiple download. We generate per-plugin checksum files under | |
| # distinct temp names (${BIN}-checksums.tmp) and upload-rename them to | |
| # plain `checksums.txt` via gh release's FILE#NAME syntax, so the | |
| # inject-preflight.py runtime check (which downloads `checksums.txt` | |
| # from each release tag) finds it under the expected path. | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| create-release: | |
| name: Create release | |
| needs: [detect, build-release] | |
| if: needs.detect.outputs.has_builds == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # contents: write is required to create the git tag and the | |
| # GitHub Release for each published plugin (see `gh release create` | |
| # below). No other job in this workflow has contents: write — | |
| # only this job, only for this purpose. The release tag itself is | |
| # immutable (the create step skips if the tag already exists), so | |
| # this permission cannot be used to rewrite previously-published | |
| # releases. Audit reviewer: this is the single point of write | |
| # access in the workflow. | |
| contents: write | |
| outputs: | |
| published_json: ${{ steps.publish.outputs.published_json }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Publish releases | |
| id: publish | |
| env: | |
| BUILD_PLUGINS: ${{ needs.detect.outputs.build_plugins_json }} | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| PUBLISHED='[]' | |
| echo "$BUILD_PLUGINS" | jq -c '.[]' | while read -r P; do | |
| NAME=$(echo "$P" | jq -r .name) | |
| BIN=$( echo "$P" | jq -r .binary_name) | |
| VER=$( echo "$P" | jq -r .version) | |
| TAG="plugins/${NAME}@${VER}" | |
| # Idempotency: skip if already released | |
| if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then | |
| echo "::notice::release $TAG already exists — skipping (immutable)" | |
| continue | |
| fi | |
| # Collect this plugin's 9 binaries from the merged artifacts dir | |
| ASSETS=$(ls artifacts/${BIN}-* 2>/dev/null | tr '\n' ' ') | |
| if [ -z "$ASSETS" ]; then | |
| echo "::error::no artifacts found for ${NAME} (pattern: ${BIN}-*)" | |
| continue | |
| fi | |
| ASSET_COUNT=$(echo "$ASSETS" | wc -w | tr -d ' ') | |
| # SHA256 manifest. Use a per-plugin temp filename in shared | |
| # artifacts/ to avoid two plugins' checksum files overwriting | |
| # each other, then upload-rename to plain `checksums.txt` | |
| # using gh release's FILE#NAME syntax so the consumer-side | |
| # download URL (`${RELEASE_BASE}/checksums.txt`) resolves. | |
| ( cd artifacts && sha256sum ${BIN}-* > "${BIN}-checksums.tmp" ) | |
| ASSETS="${ASSETS}artifacts/${BIN}-checksums.tmp#checksums.txt" | |
| echo "creating release $TAG with ${ASSET_COUNT} binaries + checksums" | |
| gh release create "$TAG" \ | |
| --repo "$REPO" \ | |
| --title "$NAME $VER" \ | |
| --notes "Auto-released from commit ${GITHUB_SHA:0:12}. | |
| Plugin: $NAME | |
| Version: $VER | |
| Binary: $BIN | |
| Targets: 9 (3 macOS + 4 Linux musl + 3 Windows MSVC) | |
| Audit: this release was built from a GitLab-synced commit on \`main\`. | |
| No PR-based ingress exists; all source vetting happens on GitLab on-prem." \ | |
| $ASSETS | |
| PUBLISHED=$(echo "$PUBLISHED" | jq \ | |
| --arg n "$NAME" --arg v "$VER" --arg t "$TAG" \ | |
| '. += [{name:$n, version:$v, tag:$t}]') | |
| done | |
| ONE_LINE=$(echo "$PUBLISHED" | jq -c .) | |
| echo "published_json=${ONE_LINE}" >> "$GITHUB_OUTPUT" | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # JOB 5 notify-failure | |
| # | |
| # If any of the above jobs fail, alert the maintainer channel so we don't | |
| # silently miss a broken release. Webhook URL configurable via secret; | |
| # when not set, falls back to printing a CI warning. | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| notify-failure: | |
| name: Notify failure | |
| needs: [verify-source, detect, build-release, create-release] | |
| if: failure() | |
| runs-on: ubuntu-latest | |
| permissions: read-all | |
| steps: | |
| - name: Send alert | |
| env: | |
| ALERT_URL: ${{ secrets.MAINTAINERS_ALERT_WEBHOOK }} | |
| run: | | |
| if [ -z "${ALERT_URL:-}" ]; then | |
| echo "::warning::MAINTAINERS_ALERT_WEBHOOK not set — failure silent" | |
| exit 0 | |
| fi | |
| curl --silent --max-time 10 --header "Content-Type: application/json" \ | |
| --data "$(jq -nc \ | |
| --arg r "$GITHUB_REPOSITORY" \ | |
| --arg run "$GITHUB_RUN_ID" \ | |
| --arg c "${GITHUB_SHA:0:12}" \ | |
| '{event:"publish_failed", repo:$r, run_id:$run, commit:$c, | |
| run_url:"https://github.com/\($r)/actions/runs/\($run)"}')" \ | |
| "$ALERT_URL" |