Skip to content

chore(deps): bump jdx/mise-action from 4.0.0 to 4.0.1 in /.github/actions/setup-tools #10883

chore(deps): bump jdx/mise-action from 4.0.0 to 4.0.1 in /.github/actions/setup-tools

chore(deps): bump jdx/mise-action from 4.0.0 to 4.0.1 in /.github/actions/setup-tools #10883

name: Automation / Labels
# SECURITY NOTE: This workflow uses pull_request_target to gain write permissions
# for labeling PRs from forks. This is safe because:
# 1. We do NOT checkout fork code (no ref: parameter pointing to fork)
# 2. We use GitHub API for file detection (no tj-actions/changed-files)
# 3. We only read PR metadata (title, author, file paths) - never execute PR code
#
# DO NOT add any step that:
# - Uses ref: ${{ github.event.pull_request.head.sha }}
# - Runs npm install/test/build on checked-out code
# - Executes scripts from the PR
on:
pull_request_target:
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
issues:
types: [opened]
push:
branches:
- "main"
paths:
- ".github/labels.yml"
- ".github/workflows/automation-labels.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}
cancel-in-progress: true
jobs:
meta:
name: "Meta"
permissions:
contents: read
runs-on: ubuntu-latest
outputs:
author: ${{ steps.author.outputs.author }}
status: ${{ steps.status.outputs.status }}
areas: ${{ steps.files_changed.outputs.areas || '[]' }}
change_type: ${{ steps.change_type.outputs.change_type }}
steps:
# NOTE: No checkout step - this is intentional for security with pull_request_target.
# We use GitHub API instead to detect changed files.
- name: Determine Author
id: author
env:
AUTHOR_LOGIN: "${{ github.event.sender.login }}"
AUTHOR_TYPE: "${{ github.event.sender.type }}"
TAMBO_TEAM_MEMBERS: |- # json
[
"alecf",
"akhileshrangani4",
"lachieh",
"MichaelMilstead",
"michaelmagan"
]
KNOWN_AI_ACTORS: |- # json
[
"charliecreates[bot]",
"CharlieHelps"
]
run: | # shell
AUTHOR="external"
if jq -e ". | index(\"${AUTHOR_LOGIN}\")" <<< "${TAMBO_TEAM_MEMBERS}" > /dev/null; then
AUTHOR="tambo-team"
elif jq -e ". | index(\"${AUTHOR_LOGIN}\")" <<< "${KNOWN_AI_ACTORS}" > /dev/null; then
AUTHOR="ai"
elif [[ "$AUTHOR_TYPE" == "Bot" || "${AUTHOR_LOGIN}" == *"[bot]"* ]]; then
AUTHOR="bot"
fi
echo "author=${AUTHOR}" >> "${GITHUB_OUTPUT}"
- name: Check Status
id: status
if: github.event_name == 'pull_request_target' || github.event_name == 'issues'
env:
EXISTING_LABELS: ${{ toJson(github.event.issue.labels || github.event.pull_request.labels || '[]') }}
AUTHOR: ${{ steps.author.outputs.author }}
run: | # shell
ISSUE_STATUS="in progress"
if [[ "${AUTHOR}" != "tambo-team" ]]; then
ISSUE_STATUS="triage"
fi
# Only apply one status label
if jq -e '.[] | select(.name | startswith("status: "))' <<< "${EXISTING_LABELS}" > /dev/null; then
ALL_STATUS_LABELS="$(jq -r '.[] | select(.name | startswith("status: ")) | .name' <<< "${EXISTING_LABELS}")"
FIRST_STATUS_LABEL=$(echo "${ALL_STATUS_LABELS}" | head -n 1)
ISSUE_STATUS="$(echo "${FIRST_STATUS_LABEL}" | sed 's/status: //')"
fi
echo "status=${ISSUE_STATUS}" >> "${GITHUB_OUTPUT}"
- name: Determine Change Type
id: change_type
if: github.event_name == 'pull_request_target'
env:
PR_TITLE: "${{ github.event.pull_request.title }}"
run: | # shell
# Extract conventional commit type from PR title (e.g., "feat(scope): description" -> "feat")
CHANGE_TYPE=""
if [[ "${PR_TITLE}" =~ ^(feat|fix|chore|docs|refactor|test|ci|perf|build|style|revert|deps)(\(.+\))?!?:.*$ ]]; then
CHANGE_TYPE="${BASH_REMATCH[1]}"
fi
echo "change_type=${CHANGE_TYPE}" >> "${GITHUB_OUTPUT}"
- name: Determine Areas via GitHub API
id: files_changed
if: github.event_name == 'pull_request_target' || github.event_name == 'push'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: | # javascript
// For push events, we need to get the files from the commit
// For pull_request_target events, we get files from the PR
const changedPathSet = new Set();
if (context.eventName === 'push') {
// Prefer compareCommits for push events to match the workflow's `paths:`
// filter semantics and avoid truncated commit payloads.
if (context.payload.before && context.payload.after) {
const { data: compareData } = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: context.payload.before,
head: context.payload.after
});
for (const file of compareData.files || []) {
changedPathSet.add(file.filename);
}
} else {
const commits = context.payload.commits || [];
core.warning(
'Push payload missing before/after; falling back to commit file lists which may be incomplete.',
);
if (
typeof context.payload.size === 'number' &&
commits.length > 0 &&
commits.length !== context.payload.size
) {
core.warning(
`Push commits payload is truncated (got ${commits.length} commits, expected ${context.payload.size}); area detection may be incomplete.`,
);
}
// Get files changed in the push commits (avoid per-commit API calls)
for (const commit of commits) {
for (const file of commit.added || []) {
changedPathSet.add(file);
}
for (const file of commit.modified || []) {
changedPathSet.add(file);
}
for (const file of commit.removed || []) {
changedPathSet.add(file);
}
}
}
} else if (context.eventName === 'pull_request_target') {
// Get files changed in the PR via API (safe - no checkout needed)
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
per_page: 100
});
for (const file of files) {
changedPathSet.add(file.filename);
}
}
const changedPaths = Array.from(changedPathSet);
// Area detection patterns (matching the previous tj-actions/changed-files config)
const areaPatterns = {
api: [/^apps\/api\//],
web: [/^apps\/web\//],
showcase: [/^showcase\//],
cli: [/^cli\//],
community: [/^community\/(?!templates)/],
templates: [/^community\/templates\//],
db: [/^packages\/db\//],
core: [/^packages\/core\//],
backend: [/^packages\/backend\//],
ui: [/^packages\/ui-registry\//],
'react-sdk': [/^react-sdk\//],
'react-ui-base': [/^packages\/react-ui-base\//],
config: [
/^packages\/eslint-config\//,
/^packages\/typescript-config\//,
/^turbo\.json$/,
/^\.nvmrc$/,
/^\.node-version$/,
/^mise\.toml$/,
/^\.prettier/,
/\.config\.(js|cjs|mjs|ts|cts|mts|json)$/
],
documentation: [
/^docs\//,
/^devdocs\//,
/\.mdx?$/,
/README\./i
],
github_actions: [
/^\.github\/workflows\//,
/^\.github\/actions\//
],
labels: [
/^\.github\/labels\.yml$/,
/^\.github\/workflows\/automation-labels\.yml$/
],
};
const areas = [];
for (const [area, patterns] of Object.entries(areaPatterns)) {
if (changedPaths.some(path => patterns.some(pattern => pattern.test(path)))) {
areas.push(area);
}
}
core.setOutput('areas', JSON.stringify(areas));
repo-labels:
name: "Repository Labels"
runs-on: ubuntu-latest
needs: [meta]
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target' }}
permissions:
contents: read # to fetch repository content
issues: write # to create and update labels
pull-requests: write # to comment on PRs
steps:
# Checkout base branch only (safe with pull_request_target - no fork code)
- name: Checkout
uses: actions/checkout@v6
- name: Update Labels
uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d # v6.0.0
if: ${{ contains(fromJson(needs.meta.outputs.areas || '[]'), 'labels') }}
with:
dry-run: ${{ github.event_name == 'pull_request_target' }}
github-token: ${{ secrets.GITHUB_TOKEN }}
skip-delete: true
yaml-file: .github/labels.yml
- name: Comment on PR about label updates
if: github.event_name == 'pull_request_target' && contains(fromJson(needs.meta.outputs.areas || '[]'), 'labels')
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
header: "## :label: Repository Labels Updated"
message: |
This PR will update the repository labels in the `.github/labels.yml`:
https://github.com/tambo-ai/tambo/pull/${{ github.event.pull_request.number }}/changes#diff-${{ github.event.pull_request.head.sha }}
- name: Delete Comment on PR if no label updates
if: github.event_name == 'pull_request_target' && !contains(fromJson(needs.meta.outputs.areas || '[]'), 'labels')
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
header: "## :label: Repository Labels Updated"
delete: true
do-not-merge:
name: "Do Not Merge"
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target'
permissions:
contents: read
steps:
- name: Check
env:
DO_NOT_MERGE: "${{contains(github.event.pull_request.labels.*.name, 'status: do not merge')}}"
run: | # shell
if [[ "${DO_NOT_MERGE}" == "true" ]]; then
echo "##[error]Cannot merge when 'status: do not merge' label is present. Remove the label to proceed."
exit 1
else
echo "No 'status: do not merge' label found. Passing check."
fi
sync-labels:
name: "Sync Labels"
needs: [meta, repo-labels]
runs-on: ubuntu-latest
if: (github.event_name == 'pull_request_target' || github.event_name == 'issues') && (github.event.action != 'labeled' && github.event.action != 'unlabeled')
permissions:
contents: read
issues: write
pull-requests: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
outputs:
added_labels: ${{ steps.determine_labels.outputs.added_labels || '' }}
steps:
# NOTE: No checkout step for speed and security - we only use gh CLI and jq (pre-installed on runners)
- name: Determine Labels
id: determine_labels
env:
EXISTING_LABELS: "${{ toJson(github.event.issue.labels || github.event.pull_request.labels || '[]') }}"
DATA_AREAS: "${{ needs.meta.outputs.areas }}"
DATA_STATUS: "${{ needs.meta.outputs.status }}"
DATA_AUTHOR: "${{ needs.meta.outputs.author }}"
DATA_CHANGE: "${{ needs.meta.outputs.change_type }}"
run: | # shell
# Determine Labels
# object of labels to add/remove
# {"label name": true (add) / false (remove)}
LABEL_CHANGES="{}"
remove_label() {
local LABEL_TO_REMOVE="$1"
LABEL_CHANGES=$(jq --arg key "$LABEL_TO_REMOVE" '. + {($key): false}' <<< "${LABEL_CHANGES}")
}
add_label() {
local LABEL_TO_ADD="$1"
LABEL_CHANGES=$(jq --arg key "$LABEL_TO_ADD" '. + {($key): true}' <<< "${LABEL_CHANGES}")
}
remove_all_by_prefix() {
local PREFIX="$1"
while IFS= read -r LABEL; do
if [[ "$LABEL" == ${PREFIX}* ]]; then
remove_label "$LABEL"
fi
done < <(jq -r '.[] | .name' <<< "${EXISTING_LABELS}")
}
label_prefix_exists() {
local PREFIX="$1"
if jq -e --arg prefix "$PREFIX" '.[] | select(.name | startswith($prefix))' <<< "${EXISTING_LABELS}" > /dev/null; then
return 0
else
return 1
fi
}
# Areas
remove_all_by_prefix "area: "
for AREA in $(jq -r '.[]' <<< "${DATA_AREAS}"); do
# skip "labels"
if [[ "${AREA}" == "labels" ]]; then continue; fi
LABEL_KEY="area: $(echo "${AREA}" | sed 's/_/ /g')"
add_label "$LABEL_KEY"
done
# Status
remove_all_by_prefix "status: "
if [[ -n "${DATA_STATUS}" ]]; then
LABEL_KEY="status: ${DATA_STATUS}"
add_label "$LABEL_KEY"
fi
# Contributor
remove_all_by_prefix "contributor: "
if [[ -n "${DATA_AUTHOR}" ]]; then
LABEL_KEY="contributor: ${DATA_AUTHOR}"
add_label "$LABEL_KEY"
fi
# Change Type
if [[ -n "${DATA_CHANGE}" ]]; then
remove_all_by_prefix "change: "
LABEL_KEY="change: ${DATA_CHANGE}"
add_label "$LABEL_KEY"
fi
echo "added_labels=$(jq -c '.' <<< "${LABEL_CHANGES}")" >> "${GITHUB_OUTPUT}"
- name: Apply Label Changes
env:
PR_OR_ISSUE_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.issue.number }}
GH_SUBCOMMAND: ${{ github.event_name == 'pull_request_target' && 'pr' || 'issue' }}
GH_REPO: ${{ github.repository }}
LABEL_CHANGES: |-
${{ steps.determine_labels.outputs.added_labels || '{}' }}
run: | # shell
# Apply Label Changes - batch all labels into a single gh command
LABELS_TO_ADD=$(jq -r '[to_entries[] | select(.value == true) | .key] | join(",")' <<< "${LABEL_CHANGES}")
LABELS_TO_REMOVE=$(jq -r '[to_entries[] | select(.value == false) | .key] | join(",")' <<< "${LABEL_CHANGES}")
echo "Adding labels:"
if [[ -n "${LABELS_TO_ADD}" ]]; then
jq -r 'to_entries[] | select(.value == true) | "- \(.key)"' <<< "${LABEL_CHANGES}"
else
echo "- none"
fi
echo "Removing labels:"
if [[ -n "${LABELS_TO_REMOVE}" ]]; then
jq -r 'to_entries[] | select(.value == false) | "- \(.key)"' <<< "${LABEL_CHANGES}"
else
echo "- none"
fi
CMD_ARGS=()
if [[ -n "${LABELS_TO_ADD}" ]]; then
CMD_ARGS+=(--add-label "${LABELS_TO_ADD}")
fi
if [[ -n "${LABELS_TO_REMOVE}" ]]; then
CMD_ARGS+=(--remove-label "${LABELS_TO_REMOVE}")
fi
if [[ ${#CMD_ARGS[@]} -gt 0 ]]; then
RESULT=$(gh "${GH_SUBCOMMAND}" edit "${PR_OR_ISSUE_NUMBER}" -R "${GH_REPO}" "${CMD_ARGS[@]}" 2>&1)
if [[ $? -ne 0 ]]; then
echo "::error title=Failed to sync labels::Failed to sync labels. Error: ${RESULT}"
fi
else
echo "No label changes to apply"
fi