name: Plugin Release on: workflow_call: inputs: version: description: 'Release version (e.g. 1.0.0)' required: true type: string node-version: description: 'Node.js version to use' required: false type: string default: '22' upstream-repo: description: 'Upstream repo to fetch appVersion from (e.g. fenio/tns-csi). Leave empty to skip.' required: false type: string default: '' secrets: RELEASE_APP_ID: description: 'GitHub App ID for creating PRs (org blocks GITHUB_TOKEN from creating PRs)' required: true RELEASE_APP_PRIVATE_KEY: description: 'GitHub App private key (PEM format)' required: true permissions: contents: write pull-requests: write concurrency: group: release cancel-in-progress: false jobs: check-secrets: runs-on: runners-privilegedescalation outputs: ready: ${{ steps.check.outputs.ready }} steps: - name: Verify RELEASE_APP_ID is configured id: check env: RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} run: | if [ -z "$RELEASE_APP_ID" ]; then echo "::notice::RELEASE_APP_ID org secret is not configured (see PRI-380). Release skipped — no artifacts will be created." echo "ready=false" >> $GITHUB_OUTPUT else echo "ready=true" >> $GITHUB_OUTPUT fi ci: needs: check-secrets if: needs.check-secrets.outputs.ready == 'true' uses: ./.github/workflows/plugin-ci.yaml with: node-version: ${{ inputs.node-version }} check-tag: needs: check-secrets if: needs.check-secrets.outputs.ready == 'true' runs-on: runners-privilegedescalation outputs: skip: ${{ steps.check.outputs.skip }} steps: - name: Check if tag already exists id: check run: | HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: Bearer ${{ github.token }}" \ "https://api.github.com/repos/${{ github.repository }}/git/refs/tags/v${{ inputs.version }}") if [ "$HTTP_CODE" = "200" ]; then echo "::notice::Tag v${{ inputs.version }} already exists. Release skipped (not an error)." echo "skip=true" >> $GITHUB_OUTPUT else echo "skip=false" >> $GITHUB_OUTPUT fi release: needs: [ci, check-tag, check-secrets] if: needs.check-secrets.outputs.ready == 'true' && needs.check-tag.outputs.skip != 'true' runs-on: runners-privilegedescalation timeout-minutes: 10 steps: - name: Validate version format run: | if [[ ! "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Error: Version must be in X.Y.Z format" exit 1 fi - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Detect package manager id: pkg-manager run: | if [ -f "pnpm-lock.yaml" ]; then echo "manager=pnpm" >> $GITHUB_OUTPUT echo "lockfile=pnpm-lock.yaml" >> $GITHUB_OUTPUT # Check for packageManager field in package.json (Corepack pinning). # pnpm/action-setup@v5 errors when packageManager is absent and no version # is specified, so use Corepack for repos that have the field pinned and # fall back to pnpm/action-setup with version: latest for repos that don't. PM=$(python3 -c "import json,sys; d=json.load(open('package.json')); print('true' if d.get('packageManager','').startswith('pnpm@') else 'false')" 2>/dev/null || echo "false") echo "has_package_manager=$PM" >> $GITHUB_OUTPUT else echo "manager=npm" >> $GITHUB_OUTPUT echo "lockfile=package-lock.json" >> $GITHUB_OUTPUT echo "has_package_manager=false" >> $GITHUB_OUTPUT fi - name: Setup Node uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} # Only enable built-in npm caching here; pnpm caching is handled below # after pnpm is installed (corepack is not available before setup-node). cache: ${{ steps.pkg-manager.outputs.manager == 'npm' && 'npm' || '' }} - name: Setup pnpm (via Corepack, reads version from packageManager field) if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'true' run: | npm install -g corepack corepack enable pnpm corepack install - name: Setup pnpm (version latest) if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'false' uses: pnpm/action-setup@v5 with: run_install: false version: latest - name: Get pnpm store directory id: pnpm-store if: steps.pkg-manager.outputs.manager == 'pnpm' run: echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Cache pnpm store if: steps.pkg-manager.outputs.manager == 'pnpm' uses: actions/cache@v5 with: path: ${{ steps.pnpm-store.outputs.dir }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm- - name: Configure Git run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Update version in package.json run: | if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then pnpm version ${{ inputs.version }} --no-git-tag-version --allow-same-version else npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version fi - name: Update artifacthub-pkg.yml run: | VERSION="${{ inputs.version }}" PKG_NAME=$(jq -r .name package.json) RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${PKG_NAME}-${VERSION}.tar.gz" sed -i "s/^version:.*/version: \"${VERSION}\"/" artifacthub-pkg.yml sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"${RELEASE_URL}\"|" artifacthub-pkg.yml - name: Update appVersion from upstream release if: inputs.upstream-repo != '' run: | APP_VERSION=$(curl -sf "https://api.github.com/repos/${{ inputs.upstream-repo }}/releases/latest" | jq -r '.tag_name | ltrimstr("v")') if [ -z "$APP_VERSION" ] || [ "$APP_VERSION" = "null" ]; then echo "::warning::Could not fetch latest upstream release, skipping appVersion update" else sed -i "s|^appVersion:.*|appVersion: \"${APP_VERSION}\"|" artifacthub-pkg.yml echo "appVersion set to ${APP_VERSION}" fi - name: Install dependencies run: | if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then pnpm install --frozen-lockfile else npm ci fi - name: Build plugin run: npx @kinvolk/headlamp-plugin build - name: Package plugin run: npx @kinvolk/headlamp-plugin package - name: Prepare release tarball run: | VERSION="${{ inputs.version }}" PKG_NAME=$(jq -r .name package.json) TARBALL="${PKG_NAME}-${VERSION}.tar.gz" # Rename tarball if headlamp-plugin produced a different name for f in *.tar.gz; do [ "$f" != "$TARBALL" ] && mv "$f" "$TARBALL" done if [ ! -f "$TARBALL" ]; then echo "Error: Expected tarball $TARBALL not found" ls -la *.tar.gz 2>/dev/null || echo "No .tar.gz files found" exit 1 fi echo "TARBALL=$TARBALL" >> $GITHUB_ENV echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV - name: Validate tarball run: | echo "Tarball: ${{ env.TARBALL }}" ls -lh "${{ env.TARBALL }}" tar -tzf "${{ env.TARBALL }}" | head -20 tar -tzf "${{ env.TARBALL }}" | grep -q "main.js" || { echo "Error: main.js not found in tarball"; exit 1; } - name: Compute checksum run: | CHECKSUM=$(sha256sum "${{ env.TARBALL }}" | awk '{print $1}') echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml - name: Commit and tag run: | VERSION="${{ inputs.version }}" BRANCH="release/v${VERSION}" # If the release branch already exists (e.g. from a failed prior run), # delete it so the re-trigger can proceed cleanly. The check-tag job # above already skips when the tag exists, so we only reach here when # the tag does NOT exist yet — safe to remove a stale branch. if git ls-remote --exit-code origin "refs/heads/$BRANCH" 2>/dev/null; then echo "::notice::Branch $BRANCH already exists — deleting for clean re-trigger." git push origin --delete "$BRANCH" fi git checkout -b "$BRANCH" git add package.json "${{ steps.pkg-manager.outputs.lockfile }}" artifacthub-pkg.yml git commit -m "release: v${VERSION}" git tag "v${VERSION}" git push origin "$BRANCH" git push origin "refs/tags/v${VERSION}" - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: "v${{ inputs.version }}" files: ${{ env.TARBALL }} fail_on_unmatched_files: false generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Generate GitHub App token id: app-token uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.RELEASE_APP_ID }} private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - name: Install GitHub CLI run: | if ! command -v gh &>/dev/null; then GH_VERSION="2.74.0" curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o /tmp/gh.tar.gz tar -xzf /tmp/gh.tar.gz -C /tmp mkdir -p "$HOME/.local/bin" mv "/tmp/gh_${GH_VERSION}_linux_amd64/bin/gh" "$HOME/.local/bin/gh" rm -rf /tmp/gh.tar.gz "/tmp/gh_${GH_VERSION}_linux_amd64" echo "$HOME/.local/bin" >> "$GITHUB_PATH" "$HOME/.local/bin/gh" --version fi - name: Create PR for version bump run: | VERSION="${{ inputs.version }}" BODY=$(printf "Automated version bump and checksum update for v%s.\n\ncc @cpfarhood" "${VERSION}") # Create PR only if an OPEN one doesn't already exist. # Note: gh pr view also finds MERGED PRs; we must check for open ones explicitly # so that a re-trigger after a stale-branch delete creates a fresh PR. OPEN_PR=$(gh pr list --base main --head "release/v${VERSION}" --state open --json number --jq '.[0].number' 2>/dev/null) if [ -z "$OPEN_PR" ]; then gh pr create \ --title "release: v${VERSION}" \ --body "$BODY" \ --base main \ --head "release/v${VERSION}" else echo "::notice::Open PR #${OPEN_PR} for release/v${VERSION} already exists — skipping creation." fi # Re-fetch open PR number (handles both new and pre-existing open PRs). PR_STATE=$(gh pr list --base main --head "release/v${VERSION}" --state open --json state --jq '.[0].state' 2>/dev/null || echo "unknown") if [ "$PR_STATE" != "OPEN" ]; then echo "No open PR for release/v${VERSION} — it may have already merged. Checking." PR_STATE=$(gh pr view "release/v${VERSION}" --json state --jq '.state' 2>/dev/null || echo "unknown") if [ "$PR_STATE" = "MERGED" ]; then echo "PR release/v${VERSION} is already merged. Skipping merge step." exit 0 fi fi # Use auto-merge only when there are pending status checks to wait for. # Valid mergeStateStatus values (gh GraphQL): # BEHIND, BLOCKED, CLEAN, DIRTY, DRAFT, HAS_HOOKS, UNKNOWN, UNSTABLE # Note: Field was renamed from mergeableState to mergeStateStatus in gh CLI. MERGE_STATE=$(gh pr view "release/v${VERSION}" --json mergeStateStatus --jq '.mergeStateStatus') if [ "$MERGE_STATE" = "BLOCKED" ]; then echo "PR is $MERGE_STATE — enabling auto-merge." gh pr merge "release/v${VERSION}" --auto --squash --delete-branch elif [ "$MERGE_STATE" = "UNKNOWN" ]; then # GitHub is still computing mergeability. Retry once after a brief wait. echo "PR is $MERGE_STATE — GitHub is computing mergeability. Retrying in 5s." sleep 5 MERGE_STATE=$(gh pr view "release/v${VERSION}" --json mergeStateStatus --jq '.mergeStateStatus') if [ "$MERGE_STATE" = "BLOCKED" ]; then echo "PR is now $MERGE_STATE — enabling auto-merge." gh pr merge "release/v${VERSION}" --auto --squash --delete-branch else echo "PR is still $MERGE_STATE after retry — merging directly." gh pr merge "release/v${VERSION}" --squash --delete-branch fi else echo "PR is $MERGE_STATE — merging directly." gh pr merge "release/v${VERSION}" --squash --delete-branch fi env: GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Verify checksums are consistent (main == tag == tarball) run: | VERSION="${{ inputs.version }}" TARBALL_CS=$(sha256sum "${{ env.TARBALL }}" | awk '{print $1}') # Checksum recorded in the tag's artifacthub-pkg.yml TAG_CS=$(git show "v${VERSION}:artifacthub-pkg.yml" 2>/dev/null | grep "archive-checksum" | awk '{print $2}' | sed 's/sha256://') # Checksum now on main (after PR merge) MAIN_CS=$(git fetch origin main 2>/dev/null; git show "origin/main:artifacthub-pkg.yml" | grep "archive-checksum" | awk '{print $2}' | sed 's/sha256://') echo "Tarball SHA256 : $TARBALL_CS" echo "Tag artifacthub: $TAG_CS" echo "Main artifacthub: $MAIN_CS" FAIL=0 [ "$TARBALL_CS" != "$TAG_CS" ] && echo "ERROR: tag checksum mismatch!" && FAIL=1 [ "$TARBALL_CS" != "$MAIN_CS" ] && echo "ERROR: main checksum mismatch!" && FAIL=1 [ "$FAIL" = "1" ] && exit 1 echo "All checksums consistent — ArtifactHub will index correctly." env: GH_TOKEN: ${{ steps.app-token.outputs.token }}