diff --git a/.github/workflows/plugin-release.yaml b/.github/workflows/plugin-release.yaml deleted file mode 100644 index 3a1e9ce..0000000 --- a/.github/workflows/plugin-release.yaml +++ /dev/null @@ -1,439 +0,0 @@ -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: ubuntu-latest - 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-token-permissions: - needs: check-secrets - if: needs.check-secrets.outputs.ready == 'true' - runs-on: ubuntu-latest - outputs: - has_write: ${{ steps.check.outputs.has_write }} - steps: - - 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: Check write permissions via API - id: check - run: | - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/${{ github.repository }}/git/refs" \ - -d '{"ref":"refs/heads/_release_check","sha":"${{ github.sha }}"}') - if [ "$HTTP_CODE" = "201" ]; then - echo "::notice::Token has write permission — cleaning up test ref." - curl -s -o /dev/null -w "%{http_code}" \ - -X DELETE \ - -H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \ - "https://api.github.com/repos/${{ github.repository }}/git/refs/heads/_release_check" - echo "has_write=true" >> $GITHUB_OUTPUT - elif [ "$HTTP_CODE" = "403" ]; then - echo "::error::Token lacks write permission. Release cannot push tags or branches." - echo "has_write=false" >> $GITHUB_OUTPUT - exit 1 - else - echo "::warning::Unexpected response ($HTTP_CODE) when checking write permission." - echo "has_write=false" >> $GITHUB_OUTPUT - exit 1 - fi - - check-tag: - needs: check-secrets - if: needs.check-secrets.outputs.ready == 'true' - runs-on: ubuntu-latest - 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, check-token-permissions] - if: needs.check-secrets.outputs.ready == 'true' && needs.check-tag.outputs.skip != 'true' && needs.check-token-permissions.outputs.has_write == 'true' - runs-on: ubuntu-latest - 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 }}" - if [ -f artifacthub-pkg.yml ]; then - PKG_NAME=$(grep '^name:' artifacthub-pkg.yml | cut -d: -f2 | tr -d ' "') - else - PKG_NAME=$(jq -r .name package.json | sed 's|^@[^/]*/||') - fi - 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: | - max_attempts=3 - attempt=1 - while [ $attempt -le $max_attempts ]; do - echo "Attempt $attempt of $max_attempts" - if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then - pnpm install --frozen-lockfile && break - else - npm ci && break - fi - if [ $attempt -lt $max_attempts ]; then - echo "::warning::Install step failed on attempt $attempt. Retrying in 5 seconds..." - sleep 5 - fi - attempt=$((attempt + 1)) - done - if [ $attempt -gt $max_attempts ]; then - echo "::error::Install step failed after $max_attempts attempts." - exit 1 - 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 }}" - # headlamp-plugin strips the @org/ prefix when naming tarballs. - # e.g. @privilegedescalation/headlamp-argocd-plugin -> headlamp-argocd-plugin - if [ -f artifacthub-pkg.yml ]; then - PKG_NAME=$(grep '^name:' artifacthub-pkg.yml | cut -d: -f2 | tr -d ' "') - else - PKG_NAME=$(jq -r .name package.json | sed 's|^@[^/]*/||') - fi - TARBALL="${PKG_NAME}-${VERSION}.tar.gz" - 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: 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: 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: ${{ steps.app-token.outputs.token }} - - - 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: | - set -o pipefail - 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}" - # Pull the number again to handle both create and pre-existing cases - OPEN_PR=$(gh pr list --base main --head "release/v${VERSION}" --state open --json number --jq '.[0].number' 2>/dev/null) - else - echo "::notice::Open PR #${OPEN_PR} for release/v${VERSION} already exists — skipping creation." - fi - # Guard: ensure we have a PR number before proceeding - if [ -z "$OPEN_PR" ]; then - echo "::error::Could not determine PR number for release/v${VERSION}." - exit 1 - fi - echo "::notice::Working with PR #${OPEN_PR}" - - # Check if PR was already merged (idempotency — safe to re-trigger after a stale branch) - MERGED_CHECK=$(gh pr view "$OPEN_PR" --json state --jq '.state' 2>/dev/null) - if [ "$MERGED_CHECK" = "MERGED" ]; then - echo "::notice::PR #${OPEN_PR} was already merged. Nothing to do." - exit 0 - fi - # Determine whether to use --auto or not based on current status. - # Retry the status check up to 3 times with exponential back-off when - # GitHub is still computing the merge state (UNKNOWN state). - MAX_RETRIES=3 - BACKOFF=3 - MERGE_STATE="" - for i in $(seq 1 $MAX_RETRIES); do - MERGE_STATE=$(gh pr view "$OPEN_PR" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null) - if [ "$MERGE_STATE" != "UNKNOWN" ]; then - break - fi - if [ $i -lt $MAX_RETRIES ]; then - echo "PR merge state is UNKNOWN (GitHub still computing). Retry ${i}/${MAX_RETRIES} in ${BACKOFF}s..." - sleep $BACKOFF - BACKOFF=$((BACKOFF * 2)) - fi - done - - if [ "$MERGE_STATE" = "BLOCKED" ] || [ "$MERGE_STATE" = "UNKNOWN" ]; then - echo "PR is $MERGE_STATE — attempting auto-merge (safe fallback, waits for branch protection checks)." - if gh pr merge "$OPEN_PR" --auto --squash --delete-branch 2>&1; then - echo "Auto-merge initiated successfully." - else - AUTO_MERGE_ERR=$? - # If --auto failed because auto-merge is disabled for this repo - # (autoMergeAllowed: false), fall back to --admin which merges - # regardless of branch protection rules. --admin requires GitHub - # App token, not GITHUB_TOKEN, so GH_TOKEN is already correct. - if gh pr merge "$OPEN_PR" --admin --squash --delete-branch 2>&1; then - echo "Auto-merge unavailable (autoMergeAllowed: false) — merged via --admin." - else - echo "::error::Both --auto and --admin merge failed. Exiting." - exit 1 - fi - fi - else - echo "PR is $MERGE_STATE — merging directly." - gh pr merge "$OPEN_PR" --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 }}