From 4accd4857c5789c52a9b7997a0dfa69eb63d0b12 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Thu, 14 May 2026 19:58:32 -0400 Subject: [PATCH 1/3] Port update-catalog validation+PR flow from main to v5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same change that landed on main as part of replacing the direct- to-main commit pattern with a validating, PR-based flow. Every integration repo's caller workflow pins to @v5 (see starter.yml line 141), so the main-branch merge didn't actually reach any caller — v5 still ran the old add-and-commit direct push. This is why entries like godaddy-dnsplugin (commit fb2f11b on the catalog repo) continued to land on integrations-catalog main with no PR after the main merge. Bringing the v5 branch up to parity ensures the next integration push routes through the validation gate and opens a labeled PR against integrations-catalog instead of force-committing to main. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/update-catalog.yml | 265 ++++++++++++++++++++++----- 1 file changed, 224 insertions(+), 41 deletions(-) diff --git a/.github/workflows/update-catalog.yml b/.github/workflows/update-catalog.yml index 9b58aae..0662285 100644 --- a/.github/workflows/update-catalog.yml +++ b/.github/workflows/update-catalog.yml @@ -1,41 +1,224 @@ -name: Update Keyfactor Integrations Catalog Entry -on: - workflow_call: - secrets: - token: - description: 'Secret token from caller workflow to access SDK repo' - required: true - -jobs: - update-catalog-entry: - runs-on: ubuntu-latest - - steps: - - name: Checkout project repo - uses: keyfactor/checkout@v4 - - - name: Checkout catalog repo - uses: keyfactor/checkout@v4 - with: - token: ${{ secrets.token }} - path: './catalog-temp/' - repository: 'Keyfactor/integrations-catalog' # Change back to integrations-catalog after testing - - - uses: Keyfactor/jinja2-action@v1.2.0-multiple-data-files - with: - template: ./catalog-temp/_integration.md.tpl - output_file: ${{ format('./catalog-temp/_integrations/{0}.md', github.event.repository.name) }} - data_file: integration-manifest.json - variables: | - repository= ${{ format('https://github.com/{0}', github.repository) }} - env: - GITHUB_TOKEN: ${{ secrets.token }} - - - uses: Keyfactor/add-and-commit@v9.1.4 - with: - author_name: 'Keyfactor' - author_email: 'keyfactor@keyfactor.github.io' - branch: 'main' - message: ${{ format('Added the manifest for {0}', github.event.repository.name) }} - add: ${{ format('_integrations/{0}.md --force', github.event.repository.name) }} - cwd: './catalog-temp/' +# ============================================================================ +# PROPOSED replacement for: Keyfactor/actions/.github/workflows/update-catalog.yml +# +# Changes from current version: +# 1. Adds validation gate (repo name filter, manifest field checks, visibility) +# 2. Replaces direct-to-main commit with PR-based flow +# 3. Handles private repos as "Coming Soon" entries (link_github=false) +# 4. Labels PRs by type: catalog-add, catalog-update, coming-soon +# ============================================================================ + +name: Update Keyfactor Integrations Catalog Entry +on: + workflow_call: + secrets: + token: + description: 'Secret token from caller workflow to access catalog repo' + required: true + +jobs: + update-catalog-entry: + runs-on: ubuntu-latest + + steps: + - name: Checkout project repo + uses: keyfactor/checkout@v4 + + - name: Checkout catalog repo + uses: keyfactor/checkout@v4 + with: + token: ${{ secrets.token }} + path: './catalog-temp/' + repository: 'Keyfactor/integrations-catalog' + + # ---------------------------------------------------------------- + # STEP 1: Validation Gate + # ---------------------------------------------------------------- + - name: Validate entry + id: validate + env: + GH_TOKEN: ${{ secrets.token }} + REPO_NAME: ${{ github.event.repository.name }} + REPO_FULL: ${{ github.repository }} + run: | + echo "### Validating: $REPO_NAME" >> "$GITHUB_STEP_SUMMARY" + + # --- Failsafe: reject -dev, -test, -staging, -poc repo names --- + # NOTE: This is a failsafe only. Developers are responsible for setting + # update_catalog=false in their manifest for non-production repos. + if echo "$REPO_NAME" | grep -qE '-(dev|test|staging|poc)$'; then + echo "::error::Repository name '$REPO_NAME' matches a non-production pattern (-dev, -test, -staging, -poc). Set update_catalog=false in your integration-manifest.json for non-production repos." + echo "rejected=true" >> "$GITHUB_OUTPUT" + echo "reject_reason=Repo name matches non-production pattern. Developers: set \`update_catalog=false\` in your manifest." >> "$GITHUB_OUTPUT" + exit 1 + fi + + # --- Validate required manifest fields --- + if [ ! -f "integration-manifest.json" ]; then + echo "::error::integration-manifest.json not found in repository root." + exit 1 + fi + + NAME=$(jq -r '.name // empty' integration-manifest.json) + TYPE=$(jq -r '.integration_type // empty' integration-manifest.json) + DESC=$(jq -r '.description // empty' integration-manifest.json) + LINK_GITHUB=$(jq -r '.link_github // "true"' integration-manifest.json | tr '[:upper:]' '[:lower:]') + + ERRORS="" + if [ -z "$NAME" ]; then + ERRORS="$ERRORS\n- Missing required field: \`name\`" + fi + if [ -z "$TYPE" ]; then + ERRORS="$ERRORS\n- Missing required field: \`integration_type\`" + fi + if [ -z "$DESC" ]; then + ERRORS="$ERRORS\n- Missing required field: \`description\`" + fi + + # Validate integration_type against allowed values + ALLOWED_TYPES="orchestrator windows-orchestrator iot-orchestrator ca-gateway anyca-plugin caplugin dns-plugin pam approval-handler orchestrator-registration metadata registration-handler alert-handler api-client terraform" + if [ -n "$TYPE" ]; then + if ! echo "$ALLOWED_TYPES" | grep -qw "$TYPE"; then + ERRORS="$ERRORS\n- Invalid \`integration_type\`: \`$TYPE\`. Allowed: $ALLOWED_TYPES" + fi + fi + + if [ -n "$ERRORS" ]; then + printf "::error::Manifest validation failed:%b\n" "$ERRORS" + exit 1 + fi + + # --- Check repo visibility --- + IS_PRIVATE=$(gh api "repos/$REPO_FULL" --jq '.private') + + if [ "$IS_PRIVATE" = "true" ] && [ "$LINK_GITHUB" = "true" ]; then + echo "::error::Repository '$REPO_FULL' is private but link_github=true. This would create a broken link on the public catalog. Set link_github=false in your manifest to create a 'Coming Soon' entry, or make the repo public first." + exit 1 + fi + + # Determine entry type for labeling + if [ "$IS_PRIVATE" = "true" ] && [ "$LINK_GITHUB" = "false" ]; then + echo "entry_type=coming-soon" >> "$GITHUB_OUTPUT" + echo "Entry type: Coming Soon (private repo)" >> "$GITHUB_STEP_SUMMARY" + elif [ -f "./catalog-temp/_integrations/$REPO_NAME.md" ]; then + echo "entry_type=catalog-update" >> "$GITHUB_OUTPUT" + echo "Entry type: Update (existing entry)" >> "$GITHUB_STEP_SUMMARY" + else + echo "entry_type=catalog-add" >> "$GITHUB_OUTPUT" + echo "Entry type: Add (new entry)" >> "$GITHUB_STEP_SUMMARY" + fi + + echo "link_github=$LINK_GITHUB" >> "$GITHUB_OUTPUT" + echo "Validation passed" >> "$GITHUB_STEP_SUMMARY" + + # ---------------------------------------------------------------- + # STEP 2: Render Template + # ---------------------------------------------------------------- + - uses: Keyfactor/jinja2-action@v1.2.0-multiple-data-files + with: + template: ./catalog-temp/_integration.md.tpl + output_file: ${{ format('./catalog-temp/_integrations/{0}.md', github.event.repository.name) }} + data_file: integration-manifest.json + variables: | + repository= ${{ format('https://github.com/{0}', github.repository) }} + env: + GITHUB_TOKEN: ${{ secrets.token }} + + # ---------------------------------------------------------------- + # STEP 3: Create PR (instead of direct commit) + # ---------------------------------------------------------------- + - name: Create or update PR + env: + GH_TOKEN: ${{ secrets.token }} + REPO_NAME: ${{ github.event.repository.name }} + ENTRY_TYPE: ${{ steps.validate.outputs.entry_type }} + working-directory: './catalog-temp/' + run: | + BRANCH="catalog-update/$REPO_NAME" + + git config user.name "Keyfactor" + git config user.email "keyfactor@keyfactor.github.io" + + # Check if branch already exists on remote + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git fetch origin "$BRANCH" + git checkout "$BRANCH" + git reset --hard origin/main + else + git checkout -b "$BRANCH" + fi + + # Stage the rendered file + git add "_integrations/$REPO_NAME.md" --force + + # Check if there are actual changes + if git diff --cached --quiet; then + echo "No changes detected — catalog entry is already up to date." + echo "### No changes" >> "$GITHUB_STEP_SUMMARY" + echo "Catalog entry for \`$REPO_NAME\` is already current." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + # Determine commit message + case "$ENTRY_TYPE" in + catalog-add) + COMMIT_MSG="Add catalog entry for $REPO_NAME" + PR_TITLE="Add integration: $REPO_NAME" + ;; + catalog-update) + COMMIT_MSG="Update catalog entry for $REPO_NAME" + PR_TITLE="Update integration: $REPO_NAME" + ;; + coming-soon) + COMMIT_MSG="Add Coming Soon catalog entry for $REPO_NAME" + PR_TITLE="Coming Soon: $REPO_NAME" + ;; + esac + + git commit -m "$COMMIT_MSG" + git push origin "$BRANCH" --force + + # Ensure the three labels this workflow uses exist (idempotent — gh + # label create exits 1 if the label is already present, which we + # swallow). Without this, gh pr create --label hard-fails when the + # label is missing in the catalog repo. + gh label create catalog-add --color "2ECC71" --description "New integration entry added to the catalog" 2>/dev/null || true + gh label create catalog-update --color "1A73E8" --description "Existing integration entry updated" 2>/dev/null || true + gh label create coming-soon --color "F39C12" --description "Private repo published as a Coming Soon entry" 2>/dev/null || true + + # Build PR body + PR_BODY=$(cat </dev/null || true) + + if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then + echo "PR #$EXISTING_PR already exists for branch $BRANCH — updated with force push." + echo "### Updated existing PR #$EXISTING_PR" >> "$GITHUB_STEP_SUMMARY" + else + gh pr create \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --label "$ENTRY_TYPE" \ + --base main \ + --head "$BRANCH" + + echo "### Created PR" >> "$GITHUB_STEP_SUMMARY" + echo "PR created for \`$REPO_NAME\` with label \`$ENTRY_TYPE\`" >> "$GITHUB_STEP_SUMMARY" + fi From e86ab225a5f4e8c0fc6c26d62a0cec4f6c74c00e Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 15 May 2026 07:51:35 -0700 Subject: [PATCH 2/3] temp: Update self refence of update-catalog to testing branch `port-update-catalog-to-v5` --- .github/workflows/starter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/starter.yml b/.github/workflows/starter.yml index 3947bca..289d944 100644 --- a/.github/workflows/starter.yml +++ b/.github/workflows/starter.yml @@ -138,7 +138,7 @@ jobs: call-update-catalog-workflow: needs: call-assign-from-json-workflow if: needs.call-assign-from-json-workflow.outputs.update_catalog == 'true' && github.ref_name == 'main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') - uses: Keyfactor/actions/.github/workflows/update-catalog.yml@v5 + uses: Keyfactor/actions/.github/workflows/update-catalog.yml@port-update-catalog-to-v5 secrets: token: ${{ secrets.token }} From 5f7826392d490bb9f4804b4220bb112fa525c7a8 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Fri, 15 May 2026 13:44:57 -0400 Subject: [PATCH 3/3] Fix two validation bugs surfaced by godaddy-dnsplugin test run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. link_github default — jq's // operator treats both null AND false as "use the default", so `.link_github // "true"` silently flipped `link_github: false` manifests to "true". On a private repo (like the godaddy-dnsplugin test) that's the exact path that gets rejected as a broken-link case. Switch to an explicit null check so a literal `false` is preserved. 2. -dev failsafe grep — pattern '-(dev|test|staging|poc)$' starts with a dash, so grep interpreted it as a flag and emitted "invalid option -- '('". Execution continues (the if-condition just evaluates false) but the failsafe wasn't actually firing — any -dev / -test / -staging / -poc repo would slip past this check. Add a -- separator so grep treats the next arg as the pattern. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/update-catalog.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-catalog.yml b/.github/workflows/update-catalog.yml index 0662285..cacee15 100644 --- a/.github/workflows/update-catalog.yml +++ b/.github/workflows/update-catalog.yml @@ -46,7 +46,7 @@ jobs: # --- Failsafe: reject -dev, -test, -staging, -poc repo names --- # NOTE: This is a failsafe only. Developers are responsible for setting # update_catalog=false in their manifest for non-production repos. - if echo "$REPO_NAME" | grep -qE '-(dev|test|staging|poc)$'; then + if echo "$REPO_NAME" | grep -qE -- '-(dev|test|staging|poc)$'; then echo "::error::Repository name '$REPO_NAME' matches a non-production pattern (-dev, -test, -staging, -poc). Set update_catalog=false in your integration-manifest.json for non-production repos." echo "rejected=true" >> "$GITHUB_OUTPUT" echo "reject_reason=Repo name matches non-production pattern. Developers: set \`update_catalog=false\` in your manifest." >> "$GITHUB_OUTPUT" @@ -62,7 +62,10 @@ jobs: NAME=$(jq -r '.name // empty' integration-manifest.json) TYPE=$(jq -r '.integration_type // empty' integration-manifest.json) DESC=$(jq -r '.description // empty' integration-manifest.json) - LINK_GITHUB=$(jq -r '.link_github // "true"' integration-manifest.json | tr '[:upper:]' '[:lower:]') + # NOTE: don't use jq's // "true" default — that operator triggers on + # both null AND false, which would silently flip link_github: false + # to "true". Use an explicit null check instead. + LINK_GITHUB=$(jq -r 'if .link_github == null then "true" else .link_github end' integration-manifest.json | tr '[:upper:]' '[:lower:]') ERRORS="" if [ -z "$NAME" ]; then