Skip to content

Add auto-leave-spaces playbook #47

Add auto-leave-spaces playbook

Add auto-leave-spaces playbook #47

name: Validate then publish to integration
on:
pull_request:
branches: [main]
permissions:
pull-requests: write
contents: read
jobs:
validate:
runs-on: ubuntu-latest
outputs:
folder_count: ${{ steps.changed.outputs.count }}
folders: ${{ steps.changed.outputs.folders }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get changed Playbook folders
id: changed
uses: actions/github-script@v8
with:
script: |
const path = require('path');
const { playbookFoldersFromFilenames } = require(path.join(process.env.GITHUB_WORKSPACE, 'scripts/ci/playbook-folders-from-pr-files.js'));
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
per_page: 100
});
const { folders, count } = playbookFoldersFromFilenames(files.map((f) => f.filename));
core.setOutput('folders', folders.join('\n'));
core.setOutput('count', count.toString());
- name: Enforce single playbook per PR
if: steps.changed.outputs.count != '0'
run: |
COUNT="${{ steps.changed.outputs.count }}"
if [ "$COUNT" -gt 1 ]; then
echo "::error::PRs must touch only one playbook folder. This PR changes $COUNT playbooks. Split into separate PRs."
echo "Changed: ${{ steps.changed.outputs.folders }}"
exit 1
fi
- name: Setup Node.js for validation scripts
if: steps.changed.outputs.count != '0'
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install npm dependencies
if: steps.changed.outputs.count != '0'
run: npm ci
- name: Validate Playbooks
id: validate
run: |
FOLDERS="${{ steps.changed.outputs.folders }}"
if [ -z "$FOLDERS" ]; then
echo "No Playbook folders changed. Skipping validation."
echo "result=pass" >> "$GITHUB_OUTPUT"
exit 0
fi
FAILED=0
REPORT=""
while IFS= read -r FOLDER; do
[ -z "$FOLDER" ] && continue
PART=$(bash scripts/ci/validate-playbook-folder.sh "$FOLDER")
EC=$?
REPORT="${REPORT}${PART}\n"
if [ "$EC" -ne 0 ]; then
FAILED=1
fi
done <<< "$FOLDERS"
echo "result=$([ "$FAILED" -eq 0 ] && echo 'pass' || echo 'fail')" >> "$GITHUB_OUTPUT"
echo -e "# Playbook Validation Results\n\n$REPORT" > validation-report.md
echo "Validation complete. Result: $([ "$FAILED" -eq 0 ] && echo 'pass' || echo 'fail')"
- name: Post validation results
uses: actions/github-script@v8
if: steps.changed.outputs.count != '0'
with:
script: |
const fs = require('fs');
const result = '${{ steps.validate.outputs.result }}';
const body = fs.readFileSync('validation-report.md', 'utf8');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Playbook Validation Results')
);
const commentBody = body + '\n\n---\n*Validation ' + (result === 'pass' ? 'passed' : 'failed') + '.*';
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
}
- name: Extract Playbook Details from APPHUB.yaml
id: playbook_details
if: steps.changed.outputs.count != '0'
run: |
FOLDERS="${{ steps.changed.outputs.folders }}"
yaml_array_items() { awk -v key="$1" '$0 ~ "^" key ":"{f=1;next} f{if($0~/^[a-zA-Z_][a-zA-Z0-9_]*:/)exit; if($0~/^[[:space:]]+-[[:space:]]/)print}' "$2"; }
yaml_scalar() { grep -E "^${2}:" "$1" | head -1 | sed -E "s/^${2}:[[:space:]]*['\"]?([^'\"]*)['\"]?[[:space:]]*(#.*)?$/\1/" | tr -d '\r'; }
FOLDER=$(echo "$FOLDERS" | head -1)
[ -z "$FOLDER" ] || [ ! -f "${FOLDER}/APPHUB.yaml" ] && exit 0
TITLE=$(yaml_scalar "${FOLDER}/APPHUB.yaml" "title" || echo "—")
TOOL=$(yaml_scalar "${FOLDER}/APPHUB.yaml" "third_party_tool" || echo "—")
TIME=$(yaml_scalar "${FOLDER}/APPHUB.yaml" "estimated_implementation_time" || echo "—")
CATS=""
while IFS= read -r line; do
[ -z "$line" ] && continue
C=$(echo "$line" | sed -E "s/^[[:space:]]*-[[:space:]]*['\"]?([^'\"]*)['\"]?.*/\1/" | tr -d ' ')
[ -n "$C" ] && CATS="${CATS}${CATS:+, }${C}"
done <<< "$(yaml_array_items "categories" "${FOLDER}/APPHUB.yaml")"
[ -z "$CATS" ] && CATS="—"
{
echo "# Playbook Details (from APPHUB.yaml)"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| **Playbook title** | ${TITLE} |"
echo "| **Third-party tool** | ${TOOL} |"
echo "| **Categories** | ${CATS} |"
echo "| **Estimated implementation time** | ${TIME} |"
} > playbook-details.md
- name: Post Playbook Details comment
if: steps.changed.outputs.count != '0' && steps.playbook_details.outcome == 'success'
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
let body;
try {
body = fs.readFileSync('playbook-details.md', 'utf8');
} catch (e) {
body = null;
}
if (!body || body.trim().length === 0) return;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Playbook Details (from APPHUB.yaml)')
);
const commentBody = body + '\n\n---\n*Auto-generated from APPHUB.yaml.*';
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
}
- name: Fail if validation failed
if: steps.changed.outputs.count != '0'
run: |
if [ "${{ steps.validate.outputs.result }}" = "fail" ]; then
echo "Validation failed. Fix the issues above and push again."
exit 1
fi
publish-integration:
needs: validate
if: needs.validate.outputs.folder_count != '0'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check for friendly_id changes
id: friendly_id_check
run: |
BASE="${{ github.event.pull_request.base.sha }}"
FOLDERS="${{ needs.validate.outputs.folders }}"
REPORT=""
HAS_CHANGES="false"
while IFS= read -r FOLDER; do
[ -z "$FOLDER" ] && continue
[ ! -f "${FOLDER}/APPHUB.yaml" ] && continue
OLD_YAML=$(git show "${BASE}:${FOLDER}/APPHUB.yaml" 2>/dev/null) || true
if [ -z "$OLD_YAML" ]; then
continue
fi
OLD_ID=$(echo "$OLD_YAML" | grep -E "^friendly_id:" | sed -E "s/^friendly_id:[[:space:]]*['\"]?([^'\"]*)['\"]?.*/\1/" | tr -d ' ')
NEW_ID=$(grep -E "^friendly_id:" "${FOLDER}/APPHUB.yaml" | sed -E "s/^friendly_id:[[:space:]]*['\"]?([^'\"]*)['\"]?.*/\1/" | tr -d ' ')
if [ -n "$OLD_ID" ] && [ -n "$NEW_ID" ] && [ "$OLD_ID" != "$NEW_ID" ]; then
REPORT="${REPORT}- **${FOLDER}**: \`${OLD_ID}\` → \`${NEW_ID}\`\n"
HAS_CHANGES="true"
echo "::warning::friendly_id changed in ${FOLDER}: ${OLD_ID} → ${NEW_ID}. Old Contentstack entry may be orphaned."
fi
done <<< "$FOLDERS"
if [ "$HAS_CHANGES" = "true" ]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo -e "# Contentstack: friendly_id changed\n\n\`friendly_id\` was changed in the following playbook(s). The previous Contentstack entry may be orphaned. Consider unpublishing or deleting it manually in Contentstack.\n\n${REPORT}" > friendly-id-changes.md
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish playbooks to Integration
id: publish
env:
CONTENTSTACK_MANAGEMENT_TOKEN: ${{ secrets.CONTENTSTACK_MANAGEMENT_TOKEN }}
CMS_API_KEY: ${{ secrets.CMS_API_KEY }}
PLAYBOOK_PREVIEW_PR_URL: ${{ github.event.pull_request.html_url }}
run: |
FOLDERS="${{ needs.validate.outputs.folders }}"
if [ -z "$FOLDERS" ]; then
echo "No Playbook folders changed. Skipping publish."
echo "count=0" >> "$GITHUB_OUTPUT"
exit 0
fi
rm -f published.jsonl
PUBLISHED=0
while IFS= read -r FOLDER; do
[ -z "$FOLDER" ] && continue
if [ ! -f "${FOLDER}/APPHUB.yaml" ]; then
echo "Skipping ${FOLDER}: no APPHUB.yaml"
continue
fi
echo "Publishing ${FOLDER} to integration..."
node scripts/publish-playbook.js "${FOLDER}" --env integration --output published.jsonl
PUBLISHED=$((PUBLISHED + 1))
done <<< "$FOLDERS"
echo "count=$PUBLISHED" >> "$GITHUB_OUTPUT"
if [ "$PUBLISHED" -eq 0 ]; then
echo "No playbooks with APPHUB.yaml were changed."
else
echo "Published ${PUBLISHED} playbook(s) to integration."
fi
- name: Comment on PR when friendly_id changed
if: steps.friendly_id_check.outputs.has_changes == 'true'
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('friendly-id-changes.md', 'utf8');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Contentstack: friendly_id changed')
);
const commentBody = body + '\n\n---\n*This PR changes `friendly_id` in one or more playbooks. The old Contentstack entry will remain; a new entry was created.*';
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
}
- name: Comment on PR when publish fails
if: failure()
uses: actions/github-script@v8
with:
script: |
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const body = [
'# Contentstack: Publish to integration failed',
'',
`The publish step failed. Check the [workflow run logs](${runUrl}) for details.`,
'',
'Common causes:',
'- Invalid or missing `APPHUB.yaml` (e.g. category/product_type slug not found in Contentstack)',
'- Contentstack API error (auth, rate limit, or service issue)',
'- Network or connectivity issue',
'',
'---',
'*Fix the issue and push again to retry.*'
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
- name: Comment on PR with publication links
if: steps.publish.outputs.count != '0'
env:
CMS_STACK_UID: ${{ secrets.CMS_STACK_UID }}
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const CMS_STACK_UID = process.env.CMS_STACK_UID || 'bltd14fd2a03236233f';
const APP_HUB_INTEGRATION = 'https://app-hub-intb.ciscospark.com/applications';
const CS_ENTRY_BASE = `https://app.contentstack.com/#!/stack/${CMS_STACK_UID}/content-type/webex_playbook_app/en-us/entry`;
let body = '# Contentstack: Published to integration\n\n';
const lines = fs.readFileSync('published.jsonl', 'utf8').trim().split('\n').filter(Boolean);
for (const line of lines) {
const { friendly_id, entry_uid } = JSON.parse(line);
body += `## \`${friendly_id}\`\n`;
body += `- **Integration**: [View in App Hub](${APP_HUB_INTEGRATION}/${friendly_id})\n`;
body += `- **Contentstack**: [Edit entry](${CS_ENTRY_BASE}/${entry_uid}/edit?branch=main)\n\n`;
}
body += '---\n*Published playbooks are available in the integration environment.*';
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Contentstack: Published to integration')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
}