From 72e9376e12ed7bc573862e8426ed1692870209f9 Mon Sep 17 00:00:00 2001 From: Jeff Roche Date: Wed, 25 Mar 2026 17:06:07 -0400 Subject: [PATCH] feat: add edge-scrum Jira config-as-code with CI automation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the edge-scrum tooling directory with: - .jira-config/ — JSON source of truth for boards, filters, projects, components, and labels managed by the OCPEDGE team - .ci/scripts/ — sync scripts that apply config changes to Jira via REST API; credentials sourced from environment variables only - .ci/schemas/ — JSON Schema definitions for all config files - GitHub Actions workflows for PR validation (schema check, README sync enforcement, dry-run preview) and post-merge apply with concurrency control and manual workflow_dispatch trigger Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/jira-sync-apply.yml | 45 +++ .github/workflows/jira-sync-pr.yml | 48 +++ .gitignore | 2 + edge-scrum/.ci/README.md | 151 +++++++ edge-scrum/.ci/schemas/boards.schema.json | 41 ++ edge-scrum/.ci/schemas/components.schema.json | 49 +++ edge-scrum/.ci/schemas/filters.schema.json | 70 ++++ edge-scrum/.ci/schemas/labels.schema.json | 28 ++ edge-scrum/.ci/schemas/projects.schema.json | 52 +++ edge-scrum/.ci/scripts/apply-changes.sh | 154 +++++++ edge-scrum/.ci/scripts/sync-boards.sh | 153 +++++++ edge-scrum/.ci/scripts/sync-components.sh | 161 ++++++++ edge-scrum/.ci/scripts/sync-filters.sh | 225 +++++++++++ edge-scrum/.ci/scripts/sync-projects.sh | 247 ++++++++++++ edge-scrum/.ci/scripts/validate-configs.sh | 98 +++++ .../.ci/scripts/validate-readme-sync.sh | 79 ++++ edge-scrum/.jira-config/README.md | 232 +++++++++++ edge-scrum/.jira-config/boards.json | 31 ++ edge-scrum/.jira-config/components.json | 91 +++++ edge-scrum/.jira-config/filters.json | 378 ++++++++++++++++++ edge-scrum/.jira-config/labels.json | 32 ++ edge-scrum/.jira-config/projects.json | 156 ++++++++ edge-scrum/README.md | 121 ++++++ 23 files changed, 2644 insertions(+) create mode 100644 .github/workflows/jira-sync-apply.yml create mode 100644 .github/workflows/jira-sync-pr.yml create mode 100644 .gitignore create mode 100644 edge-scrum/.ci/README.md create mode 100644 edge-scrum/.ci/schemas/boards.schema.json create mode 100644 edge-scrum/.ci/schemas/components.schema.json create mode 100644 edge-scrum/.ci/schemas/filters.schema.json create mode 100644 edge-scrum/.ci/schemas/labels.schema.json create mode 100644 edge-scrum/.ci/schemas/projects.schema.json create mode 100755 edge-scrum/.ci/scripts/apply-changes.sh create mode 100755 edge-scrum/.ci/scripts/sync-boards.sh create mode 100755 edge-scrum/.ci/scripts/sync-components.sh create mode 100755 edge-scrum/.ci/scripts/sync-filters.sh create mode 100755 edge-scrum/.ci/scripts/sync-projects.sh create mode 100755 edge-scrum/.ci/scripts/validate-configs.sh create mode 100755 edge-scrum/.ci/scripts/validate-readme-sync.sh create mode 100644 edge-scrum/.jira-config/README.md create mode 100644 edge-scrum/.jira-config/boards.json create mode 100644 edge-scrum/.jira-config/components.json create mode 100644 edge-scrum/.jira-config/filters.json create mode 100644 edge-scrum/.jira-config/labels.json create mode 100644 edge-scrum/.jira-config/projects.json create mode 100644 edge-scrum/README.md diff --git a/.github/workflows/jira-sync-apply.yml b/.github/workflows/jira-sync-apply.yml new file mode 100644 index 00000000..c7a3319d --- /dev/null +++ b/.github/workflows/jira-sync-apply.yml @@ -0,0 +1,45 @@ +name: Jira Config — Apply Changes + +on: + push: + branches: [main] + paths: + - 'edge-scrum/.jira-config/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: jira-sync-apply + cancel-in-progress: false + +jobs: + validate-configs: + name: Validate JSON config schemas + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install check-jsonschema + run: pip install check-jsonschema + + - name: Validate .jira-config JSON files + run: edge-scrum/.ci/scripts/validate-configs.sh + + apply: + name: Apply Jira config changes + needs: validate-configs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Apply metadata changes to Jira + run: edge-scrum/.ci/scripts/apply-changes.sh + env: + JIRA_USERNAME: ${{ secrets.JIRA_USERNAME }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + GIT_BASE_SHA: ${{ github.event.before }} + SYNC_ALL: ${{ github.event_name == 'workflow_dispatch' }} diff --git a/.github/workflows/jira-sync-pr.yml b/.github/workflows/jira-sync-pr.yml new file mode 100644 index 00000000..141b112e --- /dev/null +++ b/.github/workflows/jira-sync-pr.yml @@ -0,0 +1,48 @@ +name: Jira Config — PR Validation + +on: + pull_request: + branches: [main] + paths: + - 'edge-scrum/.jira-config/**' + +permissions: + contents: read + +jobs: + validate-configs: + name: Validate JSON config schemas + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install check-jsonschema + run: pip install check-jsonschema + + - name: Validate .jira-config JSON files + run: edge-scrum/.ci/scripts/validate-configs.sh + + validate-readme: + name: Validate README sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate README updated alongside config changes + run: edge-scrum/.ci/scripts/validate-readme-sync.sh + + dry-run: + name: Dry-run Jira sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Preview changes + run: edge-scrum/.ci/scripts/apply-changes.sh --dry-run + env: + JIRA_USERNAME: ${{ secrets.JIRA_USERNAME }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3d9876b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +**/.env \ No newline at end of file diff --git a/edge-scrum/.ci/README.md b/edge-scrum/.ci/README.md new file mode 100644 index 00000000..1adca04e --- /dev/null +++ b/edge-scrum/.ci/README.md @@ -0,0 +1,151 @@ +# CI Scripts + +This directory contains CI/CD validation and sync scripts for the edge-scrum project. + +## Validation Scripts + +### validate-readme-sync.sh + +Validates that changes to metadata JSON files in `.jira-config/` are reflected in the README. + +**Purpose:** Ensures documentation stays in sync with configuration changes. + +**Usage:** + +```bash +# Run locally +.ci/scripts/validate-readme-sync.sh + +# In GitHub Actions +- name: Validate README sync + run: ./edge-scrum/.ci/scripts/validate-readme-sync.sh +``` + +**Behavior:** + +- Detects modifications to any of the following files: + - `boards.json` + - `filters.json` + - `projects.json` + - `components.json` + - `plans.json` + - `labels.json` +- If any metadata file changed, verifies that `README.md` was also updated +- Exits with error code 1 if README wasn't updated alongside metadata changes +- Exits with code 0 if no metadata changed or if both metadata and README changed + +**Environment Variables:** + +- `GITHUB_BASE_REF` - GitHub Actions PR base branch (auto-detected) +- `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` - GitLab CI target branch (auto-detected) +- Falls back to comparing against `origin/main` for local development + +## Sync Scripts + +These scripts apply metadata changes from JSON files to Jira via REST API. + +### apply-changes.sh + +**Orchestration script** that detects changed metadata files and applies them to Jira. + +**Purpose:** Main entry point for syncing metadata changes to Jira. + +**Usage:** + +```bash +# Dry run (preview changes) +.ci/scripts/apply-changes.sh --dry-run + +# Apply changes +.ci/scripts/apply-changes.sh + +# In GitHub Actions +- name: Apply metadata changes to Jira + run: ./edge-scrum/.ci/scripts/apply-changes.sh + env: + JIRA_URL: ${{ secrets.JIRA_URL }} + JIRA_USERNAME: ${{ secrets.JIRA_USERNAME }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} +``` + +**Behavior:** + +- Detects which metadata files changed (boards.json, filters.json, projects.json, components.json) +- Calls the appropriate sync script for each changed file +- Passes `--dry-run` flag to all sync scripts if provided +- Exits with code 1 if any sync script fails +- Skips `labels.json` (no formal API) + +### sync-boards.sh + +Updates board properties in Jira. + +**API:** `PUT /rest/agile/1.0/board/{boardId}/properties/{propertyKey}` + +**Updates:** Board properties (e.g., roadmaps features, child issue planning) + +**Note:** Only properties can be updated via API. Structural configuration (columns, estimation, ranking) requires Jira UI. + +### sync-filters.sh + +Updates filter metadata, JQL, and permissions in Jira. + +**API:** `PUT /rest/api/2/filter/{filterId}` + +**Updates:** Filter name, description, JQL, edit permissions, share permissions + +**Note:** Filter ownership transfers require Jira UI. + +### sync-projects.sh + +Updates project metadata in Jira. + +**API:** `PUT /rest/api/2/project/{projectKey}` + +**Updates:** Project name, description, lead, assigneeType + +**Note:** Role configuration is listed in JSON but is UI-only (cannot be updated via API). + +### sync-components.sh + +Updates component metadata in Jira. + +**API:** `PUT /rest/api/2/component/{componentId}` + +**Updates:** Component name, description + +**Note:** Only updates components with `component_id` in the JSON. Entries without `component_id` must be created manually. + +## Common Features + +All sync scripts share these features: + +- **`--dry-run` flag:** Preview changes without applying them +- **Credentials:** Load from `edge-scrum/.env` (JIRA_URL, JIRA_USERNAME, JIRA_API_TOKEN) +- **Error handling:** Exit with code 1 if any updates fail +- **Validation:** Check prerequisites (jq, curl, config files, env vars) +- **Logging:** Clear, color-coded output showing what's being updated +- **Safety:** Only update existing entities (no create/delete operations) + +## Prerequisites + +```bash +# Install dependencies +sudo dnf install jq curl git # Fedora/RHEL +sudo apt install jq curl git # Debian/Ubuntu + +# Configure credentials +cat > edge-scrum/.env </dev/null && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." &>/dev/null && pwd)" +CONFIG_DIR="${REPO_ROOT}/.jira-config" +BOARDS_JSON="${CONFIG_DIR}/boards.json" + +DRY_RUN=false +EXIT_CODE=0 + +# Parse command-line arguments +for arg in "$@"; do + case $arg in + --dry-run) + DRY_RUN=true + shift + ;; + *) + echo "Unknown argument: $arg" >&2 + echo "Usage: $0 [--dry-run]" >&2 + exit 1 + ;; + esac +done + +# Verify dependencies +if ! command -v jq &>/dev/null; then + echo "Error: jq is required but not installed" >&2 + exit 1 +fi + +if ! command -v curl &>/dev/null; then + echo "Error: curl is required but not installed" >&2 + exit 1 +fi + +# Verify files exist +if [[ ! -f "${BOARDS_JSON}" ]]; then + echo "Error: boards.json not found at ${BOARDS_JSON}" >&2 + exit 1 +fi + +# Validate required environment variables +JIRA_URL="${JIRA_URL:-https://redhat.atlassian.net}" +JIRA_URL="${JIRA_URL%/}" +: "${JIRA_USERNAME:?JIRA_USERNAME must be set}" +: "${JIRA_API_TOKEN:?JIRA_API_TOKEN must be set}" +JIRA_AUTH=$(printf '%s:%s' "${JIRA_USERNAME}" "${JIRA_API_TOKEN}" | base64 -w0) + +# Validate boards.json +if ! jq -e '.boards' "${BOARDS_JSON}" &>/dev/null; then + echo "Error: Invalid boards.json format - missing 'boards' array" >&2 + exit 1 +fi + +# Secure temporary directory (cleaned up on exit) +TMPDIR=$(mktemp -d) +trap 'rm -rf "${TMPDIR}"' EXIT + +echo "Syncing Jira board properties from ${BOARDS_JSON}" +if [[ "${DRY_RUN}" == "true" ]]; then + echo "[DRY RUN MODE] No changes will be made" +fi +echo + +# Get board count +BOARD_COUNT=$(jq '.boards | length' "${BOARDS_JSON}") +echo "Found ${BOARD_COUNT} board(s) to process" +echo + +# Process each board +for i in $(seq 0 $((BOARD_COUNT - 1))); do + BOARD_ID=$(jq -r ".boards[${i}].id" "${BOARDS_JSON}") + BOARD_NAME=$(jq -r ".boards[${i}].name" "${BOARDS_JSON}") + + echo "Processing board: ${BOARD_NAME} (ID: ${BOARD_ID})" + + # Get properties object + PROPERTIES=$(jq -r ".boards[${i}].properties" "${BOARDS_JSON}") + + # Check if properties object is empty + if [[ "${PROPERTIES}" == "{}" ]] || [[ "${PROPERTIES}" == "null" ]]; then + echo " No properties to sync" + echo + continue + fi + + # Get property keys + PROPERTY_KEYS=$(echo "${PROPERTIES}" | jq -r 'keys[]') + + if [[ -z "${PROPERTY_KEYS}" ]]; then + echo " No properties to sync" + echo + continue + fi + + # Process each property + while IFS= read -r PROPERTY_KEY; do + PROPERTY_VALUE=$(echo "${PROPERTIES}" | jq -r ".\"${PROPERTY_KEY}\"") + + echo " Updating property: ${PROPERTY_KEY} = ${PROPERTY_VALUE}" + + if [[ "${DRY_RUN}" == "true" ]]; then + echo " [DRY RUN] Would PUT to: ${JIRA_URL}/rest/agile/1.0/board/${BOARD_ID}/properties/${PROPERTY_KEY}" + echo " [DRY RUN] Payload: ${PROPERTY_VALUE}" + else + # Make the API request + HTTP_CODE=$(curl -s -w "%{http_code}" -o "${TMPDIR}/response.json" \ + -X PUT \ + -H "Content-Type: application/json" \ + -H "Authorization: Basic ${JIRA_AUTH}" \ + -d "${PROPERTY_VALUE}" \ + "${JIRA_URL}/rest/agile/1.0/board/${BOARD_ID}/properties/${PROPERTY_KEY}") + + if [[ "${HTTP_CODE}" -ge 200 ]] && [[ "${HTTP_CODE}" -lt 300 ]]; then + echo " ✓ Success (HTTP ${HTTP_CODE})" + else + echo " ✗ Failed (HTTP ${HTTP_CODE})" >&2 + if [[ -f "${TMPDIR}/response.json" ]]; then + echo " Response: $(cat "${TMPDIR}/response.json")" >&2 + fi + EXIT_CODE=1 + fi + fi + done <<< "${PROPERTY_KEYS}" + + echo +done + +if [[ "${EXIT_CODE}" -eq 0 ]]; then + if [[ "${DRY_RUN}" == "true" ]]; then + echo "Dry run completed successfully" + else + echo "All board properties synced successfully" + fi +else + echo "Some properties failed to sync - see errors above" >&2 +fi + +exit "${EXIT_CODE}" diff --git a/edge-scrum/.ci/scripts/sync-components.sh b/edge-scrum/.ci/scripts/sync-components.sh new file mode 100755 index 00000000..1d9a5007 --- /dev/null +++ b/edge-scrum/.ci/scripts/sync-components.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# +# sync-components.sh +# +# Synchronizes Jira components from .jira-config/components.json to Jira API. +# Only updates existing components (create/delete are manual operations). +# +# Usage: +# ./sync-components.sh [--dry-run] +# +# Environment variables required (from edge-scrum/.env): +# JIRA_URL - Jira instance URL +# JIRA_USERNAME - Jira username +# JIRA_API_TOKEN - Jira API token + +set -euo pipefail + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +# Configuration paths +CONFIG_FILE="${REPO_ROOT}/.jira-config/components.json" + +# Dry run flag +DRY_RUN=false + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--dry-run]" + exit 1 + ;; + esac +done + +# Validate required environment variables +JIRA_URL="${JIRA_URL:-https://redhat.atlassian.net}" +JIRA_URL="${JIRA_URL%/}" +: "${JIRA_USERNAME:?JIRA_USERNAME must be set}" +: "${JIRA_API_TOKEN:?JIRA_API_TOKEN must be set}" +JIRA_AUTH=$(printf '%s:%s' "${JIRA_USERNAME}" "${JIRA_API_TOKEN}" | base64 -w0) + +# Validate config file exists +if [[ ! -f "${CONFIG_FILE}" ]]; then + echo "Error: Configuration file not found at ${CONFIG_FILE}" + exit 1 +fi + +# Validate jq is installed +if ! command -v jq &> /dev/null; then + echo "Error: jq is required but not installed" + exit 1 +fi + +# Validate curl is installed +if ! command -v curl &> /dev/null; then + echo "Error: curl is required but not installed" + exit 1 +fi + +# Secure temporary directory (cleaned up on exit) +TMPDIR=$(mktemp -d) +trap 'rm -rf "${TMPDIR}"' EXIT + +# Track if any updates failed +UPDATE_FAILED=false + +# Read components from config file +COMPONENTS=$(jq -c '.components[]' "${CONFIG_FILE}") + +echo "=====================================" +echo "Jira Component Sync" +echo "=====================================" +if [[ "${DRY_RUN}" == "true" ]]; then + echo "MODE: DRY RUN (no changes will be made)" +else + echo "MODE: LIVE UPDATE" +fi +echo "Config: ${CONFIG_FILE}" +echo "Jira: ${JIRA_URL}" +echo "=====================================" +echo + +# Process each component +while IFS= read -r component; do + COMPONENT_NAME=$(echo "${component}" | jq -r '.name') + COMPONENT_DESC=$(echo "${component}" | jq -r '.description') + + echo "Processing component: ${COMPONENT_NAME}" + echo " Description: ${COMPONENT_DESC}" + + # Process each project association + PROJECTS=$(echo "${component}" | jq -c '.projects[]') + + while IFS= read -r project; do + COMPONENT_ID=$(echo "${project}" | jq -r '.component_id // empty') + PROJECT_KEY=$(echo "${project}" | jq -r '.project_key') + + # Skip if no component_id (not yet created) + if [[ -z "${COMPONENT_ID}" ]]; then + echo " ⚠ Skipping ${PROJECT_KEY}: no component_id (component not yet created)" + continue + fi + + echo " → Updating component ${COMPONENT_ID} in project ${PROJECT_KEY}" + + # Build the update payload + UPDATE_PAYLOAD=$(jq -n \ + --arg name "${COMPONENT_NAME}" \ + --arg description "${COMPONENT_DESC}" \ + '{ + name: $name, + description: $description + }') + + if [[ "${DRY_RUN}" == "true" ]]; then + echo " [DRY RUN] Would update with payload:" + echo "${UPDATE_PAYLOAD}" | jq '.' + else + # Make the API call + HTTP_CODE=$(curl -s -w "%{http_code}" -o "${TMPDIR}/response.json" \ + -X PUT \ + -H "Content-Type: application/json" \ + -H "Authorization: Basic ${JIRA_AUTH}" \ + -d "${UPDATE_PAYLOAD}" \ + "${JIRA_URL}/rest/api/2/component/${COMPONENT_ID}") + + if [[ "${HTTP_CODE}" -eq 200 ]]; then + echo " ✓ Successfully updated component ${COMPONENT_ID}" + else + echo " ✗ Failed to update component ${COMPONENT_ID} (HTTP ${HTTP_CODE})" + echo " Response:" + jq '.' "${TMPDIR}/response.json" 2>/dev/null || cat "${TMPDIR}/response.json" + UPDATE_FAILED=true + fi + fi + + done <<< "${PROJECTS}" + + echo + +done <<< "${COMPONENTS}" + +echo "=====================================" +if [[ "${DRY_RUN}" == "true" ]]; then + echo "Dry run complete. No changes were made." + exit 0 +elif [[ "${UPDATE_FAILED}" == "true" ]]; then + echo "Component sync completed with errors." + exit 1 +else + echo "All components synchronized successfully." + exit 0 +fi diff --git a/edge-scrum/.ci/scripts/sync-filters.sh b/edge-scrum/.ci/scripts/sync-filters.sh new file mode 100755 index 00000000..813e68ec --- /dev/null +++ b/edge-scrum/.ci/scripts/sync-filters.sh @@ -0,0 +1,225 @@ +#!/usr/bin/env bash +# +# sync-filters.sh - Sync Jira filter metadata from filters.json to Jira API +# +# Usage: +# ./sync-filters.sh # Update filters in Jira +# ./sync-filters.sh --dry-run # Print what would be updated without making changes +# +# Requirements: +# - jq (JSON processor) +# - curl (HTTP client) +# - edge-scrum/.env with JIRA_URL, JIRA_USERNAME, JIRA_API_TOKEN +# - edge-scrum/.jira-config/filters.json +# +# Exit codes: +# 0 - Success +# 1 - Error (missing dependencies, API failures, etc.) + +set -euo pipefail + +# Colors for output +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly NC='\033[0m' # No Color + +# Determine script directory and repository root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +# Configuration paths +readonly FILTERS_JSON="${REPO_ROOT}/.jira-config/filters.json" + +# Parse command line arguments +DRY_RUN=false +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true +fi + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $*" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $*" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $*" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $*" >&2 +} + +# Check if required commands are available +check_dependencies() { + local missing_deps=() + + for cmd in jq curl; do + if ! command -v "$cmd" &> /dev/null; then + missing_deps+=("$cmd") + fi + done + + if [[ ${#missing_deps[@]} -gt 0 ]]; then + log_error "Missing required dependencies: ${missing_deps[*]}" + log_error "Please install missing dependencies and try again." + return 1 + fi +} + +# Validate required environment variables +validate_env() { + JIRA_URL="${JIRA_URL:-https://redhat.atlassian.net}" + JIRA_URL="${JIRA_URL%/}" + : "${JIRA_USERNAME:?JIRA_USERNAME must be set}" + : "${JIRA_API_TOKEN:?JIRA_API_TOKEN must be set}" + JIRA_AUTH=$(printf '%s:%s' "${JIRA_USERNAME}" "${JIRA_API_TOKEN}" | base64 -w0) +} + +# Validate filters.json file exists and is valid JSON +validate_filters_file() { + if [[ ! -f "$FILTERS_JSON" ]]; then + log_error "Filters file not found: $FILTERS_JSON" + return 1 + fi + + if ! jq empty "$FILTERS_JSON" 2>/dev/null; then + log_error "Invalid JSON in $FILTERS_JSON" + return 1 + fi +} + +# Update a single filter via Jira API +update_filter() { + local filter_id="$1" + local filter_name="$2" + local filter_description="$3" + local filter_jql="$4" + local edit_permissions="$5" + local share_permissions="$6" + + log_info "Processing filter ${filter_id}: ${filter_name}" + + # Build the JSON payload for the filter update + local payload + payload=$(jq -n \ + --arg name "$filter_name" \ + --arg description "$filter_description" \ + --arg jql "$filter_jql" \ + --argjson editPermissions "$edit_permissions" \ + --argjson sharePermissions "$share_permissions" \ + '{ + name: $name, + description: $description, + jql: $jql, + editPermissions: $editPermissions, + sharePermissions: $sharePermissions + }') + + if $DRY_RUN; then + echo "" + log_warn "[DRY RUN] Would update filter ${filter_id}:" + echo " Name: ${filter_name}" + echo " Description: ${filter_description}" + echo " JQL: ${filter_jql}" + echo " Edit Permissions: $(echo "$edit_permissions" | jq -c '.')" + echo " Share Permissions: $(echo "$share_permissions" | jq -c '.')" + echo "" + return 0 + fi + + # Make the API request + local response + local http_code + + response=$(curl -s -w "\n%{http_code}" \ + -X PUT \ + -H "Content-Type: application/json" \ + -H "Authorization: Basic ${JIRA_AUTH}" \ + -d "$payload" \ + "${JIRA_URL}/rest/api/2/filter/${filter_id}") + + # Extract HTTP status code (last line) + http_code=$(echo "$response" | tail -n 1) + + # Extract response body (everything except last line) + local response_body + response_body=$(echo "$response" | sed '$d') + + # Check if update was successful + if [[ "$http_code" -ge 200 ]] && [[ "$http_code" -lt 300 ]]; then + log_success "Updated filter ${filter_id}: ${filter_name}" + return 0 + else + log_error "Failed to update filter ${filter_id}: ${filter_name}" + log_error "HTTP Status: ${http_code}" + log_error "Response: ${response_body}" + return 1 + fi +} + +# Main execution +main() { + local exit_code=0 + + log_info "Starting Jira filter sync..." + echo "" + + # Pre-flight checks + check_dependencies || exit 1 + validate_env + validate_filters_file || exit 1 + + if $DRY_RUN; then + log_warn "DRY RUN MODE - No changes will be made to Jira" + echo "" + fi + + # Get filter count + local filter_count + filter_count=$(jq '.filters | length' "$FILTERS_JSON") + log_info "Found ${filter_count} filters to sync" + echo "" + + # Process each filter + local index=0 + while [[ $index -lt $filter_count ]]; do + # Extract filter data using jq + local filter_id filter_name filter_description filter_jql edit_perms share_perms + + filter_id=$(jq -r ".filters[$index].id" "$FILTERS_JSON") + filter_name=$(jq -r ".filters[$index].name" "$FILTERS_JSON") + filter_description=$(jq -r ".filters[$index].description" "$FILTERS_JSON") + filter_jql=$(jq -r ".filters[$index].jql" "$FILTERS_JSON") + edit_perms=$(jq -c ".filters[$index].editPermissions" "$FILTERS_JSON") + share_perms=$(jq -c ".filters[$index].sharePermissions" "$FILTERS_JSON") + + # Update the filter + if ! update_filter "$filter_id" "$filter_name" "$filter_description" "$filter_jql" "$edit_perms" "$share_perms"; then + exit_code=1 + fi + + ((index++)) + done + + echo "" + if [[ $exit_code -eq 0 ]]; then + if $DRY_RUN; then + log_success "Dry run complete - ${filter_count} filters would be updated" + else + log_success "Successfully synced ${filter_count} filters" + fi + else + log_error "Filter sync completed with errors" + fi + + exit $exit_code +} + +# Run main function +main diff --git a/edge-scrum/.ci/scripts/sync-projects.sh b/edge-scrum/.ci/scripts/sync-projects.sh new file mode 100755 index 00000000..9bb14ada --- /dev/null +++ b/edge-scrum/.ci/scripts/sync-projects.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# +# sync-projects.sh +# +# Synchronizes project metadata from .jira-config/projects.json to Jira via REST API. +# Updates project name, description, lead, and assigneeType for existing projects. +# +# Usage: +# ./sync-projects.sh [--dry-run] +# +# Options: +# --dry-run Print what would be updated without making API calls +# +# Prerequisites: +# - jq installed +# - curl installed +# - .env file with JIRA_URL, JIRA_USERNAME, JIRA_API_TOKEN +# +# Note: +# - Only updates existing projects (does not create or delete) +# - Role configuration is UI-only and cannot be updated via API +# + +set -euo pipefail + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +# Configuration files +PROJECTS_JSON="${REPO_ROOT}/.jira-config/projects.json" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Track errors +EXIT_CODE=0 + +# Parse command line arguments +DRY_RUN=false +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true +fi + +# +# Print functions +# +info() { + echo -e "${BLUE}[INFO]${NC} $*" +} + +success() { + echo -e "${GREEN}[SUCCESS]${NC} $*" +} + +warning() { + echo -e "${YELLOW}[WARNING]${NC} $*" +} + +error() { + echo -e "${RED}[ERROR]${NC} $*" >&2 + EXIT_CODE=1 +} + +# +# Validation +# +validate_prerequisites() { + local missing=() + + # Check for required commands + if ! command -v jq &> /dev/null; then + missing+=("jq") + fi + + if ! command -v curl &> /dev/null; then + missing+=("curl") + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + error "Missing required commands: ${missing[*]}" + error "Please install missing dependencies and try again" + exit 1 + fi + + # Check for required files + if [[ ! -f "${PROJECTS_JSON}" ]]; then + error "Projects configuration not found: ${PROJECTS_JSON}" + exit 1 + fi +} + +# +# Validate required environment variables +# +validate_env() { + JIRA_URL="${JIRA_URL:-https://redhat.atlassian.net}" + JIRA_URL="${JIRA_URL%/}" + : "${JIRA_USERNAME:?JIRA_USERNAME must be set}" + : "${JIRA_API_TOKEN:?JIRA_API_TOKEN must be set}" + JIRA_AUTH=$(printf '%s:%s' "${JIRA_USERNAME}" "${JIRA_API_TOKEN}" | base64 -w0) +} + +# +# Update a single project +# +update_project() { + local project_key="$1" + local project_name="$2" + local project_description="$3" + local project_lead_id="$4" + local project_assignee_type="$5" + + info "Processing project: ${project_key}" + + # Build the JSON payload + local payload + payload=$(jq -n \ + --arg name "${project_name}" \ + --arg description "${project_description}" \ + --arg leadAccountId "${project_lead_id}" \ + --arg assigneeType "${project_assignee_type}" \ + '{ + name: $name, + description: $description, + leadAccountId: $leadAccountId, + assigneeType: $assigneeType + }') + + # Print what will be updated + echo " Name: ${project_name}" + echo " Description: ${project_description}" + echo " Lead ID: ${project_lead_id}" + echo " Assignee Type: ${project_assignee_type}" + + if [[ "${DRY_RUN}" == true ]]; then + warning " [DRY-RUN] Would update project ${project_key}" + return 0 + fi + + # Make the API call + local response + local http_code + + response=$(curl -s -w "\n%{http_code}" \ + -X PUT \ + -H "Content-Type: application/json" \ + -H "Authorization: Basic ${JIRA_AUTH}" \ + -d "${payload}" \ + "${JIRA_URL}/rest/api/2/project/${project_key}") + + # Extract HTTP status code (last line) + http_code=$(echo "${response}" | tail -n 1) + # Extract response body (all but last line) + local response_body + response_body=$(echo "${response}" | sed '$d') + + # Check response + if [[ "${http_code}" -ge 200 && "${http_code}" -lt 300 ]]; then + success " Updated project ${project_key}" + else + error " Failed to update project ${project_key} (HTTP ${http_code})" + if [[ -n "${response_body}" ]]; then + error " Response: ${response_body}" + fi + fi +} + +# +# Main processing +# +process_projects() { + local project_count + project_count=$(jq '.projects | length' "${PROJECTS_JSON}") + + info "Found ${project_count} project(s) in ${PROJECTS_JSON}" + echo "" + + # Iterate over each project + for i in $(seq 0 $((project_count - 1))); do + local key + local name + local description + local lead_id + local assignee_type + + # Extract project fields + key=$(jq -r ".projects[${i}].key" "${PROJECTS_JSON}") + name=$(jq -r ".projects[${i}].name" "${PROJECTS_JSON}") + description=$(jq -r ".projects[${i}].description // \"\"" "${PROJECTS_JSON}") + lead_id=$(jq -r ".projects[${i}].lead.id" "${PROJECTS_JSON}") + assignee_type=$(jq -r ".projects[${i}].assigneeType" "${PROJECTS_JSON}") + + # Validate required fields + if [[ -z "${key}" || "${key}" == "null" ]]; then + error "Project at index ${i} is missing 'key' field" + continue + fi + + if [[ -z "${name}" || "${name}" == "null" ]]; then + error "Project ${key} is missing 'name' field" + continue + fi + + if [[ -z "${lead_id}" || "${lead_id}" == "null" ]]; then + error "Project ${key} is missing 'lead.id' field" + continue + fi + + # Update the project + update_project "${key}" "${name}" "${description}" "${lead_id}" "${assignee_type}" + echo "" + done + + # Note about roles + if [[ "${project_count}" -gt 0 ]]; then + info "Note: Role configuration is listed in projects.json but is UI-only" + info "Role assignments cannot be updated via the Jira REST API" + fi +} + +# +# Main execution +# +main() { + if [[ "${DRY_RUN}" == true ]]; then + warning "Running in DRY-RUN mode - no changes will be made" + echo "" + fi + + validate_prerequisites + validate_env + process_projects + + if [[ "${EXIT_CODE}" -ne 0 ]]; then + error "Some projects failed to update" + exit 1 + fi + + success "Project sync completed successfully" +} + +main diff --git a/edge-scrum/.ci/scripts/validate-configs.sh b/edge-scrum/.ci/scripts/validate-configs.sh new file mode 100755 index 00000000..9444dae2 --- /dev/null +++ b/edge-scrum/.ci/scripts/validate-configs.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# +# validate-configs.sh +# +# Validates .jira-config JSON files against their schemas and checks for +# duplicate IDs/names within each file. +# +# Usage: +# ./validate-configs.sh +# +# Requirements: +# - jq +# - check-jsonschema (pip install check-jsonschema) + +set -euo pipefail + +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly BLUE='\033[0;34m' +readonly NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +CONFIG_DIR="${REPO_ROOT}/.jira-config" +SCHEMA_DIR="${SCRIPT_DIR}/../schemas" + +ERRORS=0 + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +check_dependencies() { + local missing=() + for cmd in jq check-jsonschema; do + if ! command -v "$cmd" &>/dev/null; then + missing+=("$cmd") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + log_error "Missing required commands: ${missing[*]}" + exit 1 + fi +} + +# Validate a config file against its schema, then check for duplicate values. +# Args: config_file schema_file array_key id_field +validate_file() { + local config_file="$1" + local schema_file="$2" + local array_key="$3" + local id_field="$4" + local name + name=$(basename "$config_file") + + log_info "Validating ${name}..." + + if ! check-jsonschema --schemafile "$schema_file" "$config_file"; then + ERRORS=$((ERRORS + 1)) + return + fi + + local dupes + dupes=$(jq -r --arg key "$array_key" --arg field "$id_field" \ + '[.[$key][][$field]] | group_by(.) | map(select(length > 1)) | map(.[0])[]' \ + "$config_file") + + if [[ -n "$dupes" ]]; then + log_error "[${name}] Duplicate ${id_field}s: $(echo "$dupes" | tr '\n' ' ')" + ERRORS=$((ERRORS + 1)) + return + fi + + log_success "${name} is valid" +} + +main() { + check_dependencies + log_info "Validating .jira-config files..." + echo "" + + validate_file "${CONFIG_DIR}/boards.json" "${SCHEMA_DIR}/boards.schema.json" "boards" "id" + validate_file "${CONFIG_DIR}/filters.json" "${SCHEMA_DIR}/filters.schema.json" "filters" "id" + validate_file "${CONFIG_DIR}/projects.json" "${SCHEMA_DIR}/projects.schema.json" "projects" "id" + validate_file "${CONFIG_DIR}/components.json" "${SCHEMA_DIR}/components.schema.json" "components" "name" + validate_file "${CONFIG_DIR}/labels.json" "${SCHEMA_DIR}/labels.schema.json" "labels" "name" + + echo "" + if [[ $ERRORS -eq 0 ]]; then + log_success "All config files valid" + exit 0 + else + log_error "Validation failed: ${ERRORS} error(s) found" + exit 1 + fi +} + +main diff --git a/edge-scrum/.ci/scripts/validate-readme-sync.sh b/edge-scrum/.ci/scripts/validate-readme-sync.sh new file mode 100755 index 00000000..dafd3c76 --- /dev/null +++ b/edge-scrum/.ci/scripts/validate-readme-sync.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# validate-readme-sync.sh +# Validates that changes to metadata JSON files are reflected in the README + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EDGE_SCRUM_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +JIRA_CONFIG_DIR="${EDGE_SCRUM_DIR}/.jira-config" +README_FILE="${JIRA_CONFIG_DIR}/README.md" + +# Metadata files to check +METADATA_FILES=( + "boards.json" + "filters.json" + "projects.json" + "components.json" + "plans.json" + "labels.json" +) + +# Determine base ref for comparison +# In CI, use the base branch (e.g., main, master) +# Locally, use the merge base with main +if [ -n "${GITHUB_BASE_REF:-}" ]; then + # GitHub Actions PR context + BASE_REF="origin/${GITHUB_BASE_REF}" +elif [ -n "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-}" ]; then + # GitLab CI PR context + BASE_REF="origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" +else + # Local development - compare against main + BASE_REF="$(git merge-base HEAD origin/main 2>/dev/null || echo 'HEAD~1')" +fi + +echo "Comparing against base: ${BASE_REF}" + +# Check if any metadata files have been modified +metadata_modified=false +modified_files=() + +for file in "${METADATA_FILES[@]}"; do + file_path="${JIRA_CONFIG_DIR}/${file}" + + # Check if file exists and has been modified + if git diff --name-only "${BASE_REF}..HEAD" | grep -q "^edge-scrum/.jira-config/${file}$"; then + metadata_modified=true + modified_files+=("${file}") + echo "✓ Detected change in ${file}" + fi +done + +# If no metadata files were modified, exit successfully +if [ "${metadata_modified}" = false ]; then + echo "✓ No metadata file changes detected. Validation passed." + exit 0 +fi + +# Check if README was also modified +if git diff --name-only "${BASE_REF}..HEAD" | grep -q "^edge-scrum/.jira-config/README.md$"; then + echo "✓ README.md was updated alongside metadata changes. Validation passed." + echo "" + echo "Modified metadata files:" + for file in "${modified_files[@]}"; do + echo " - ${file}" + done + exit 0 +else + echo "✗ ERROR: Metadata files were modified but README.md was not updated." + echo "" + echo "Modified metadata files:" + for file in "${modified_files[@]}"; do + echo " - ${file}" + done + echo "" + echo "Please update edge-scrum/.jira-config/README.md to reflect these changes." + echo "The README should include human-readable lists of all items from the metadata files." + exit 1 +fi diff --git a/edge-scrum/.jira-config/README.md b/edge-scrum/.jira-config/README.md new file mode 100644 index 00000000..d9f30d2d --- /dev/null +++ b/edge-scrum/.jira-config/README.md @@ -0,0 +1,232 @@ +# Jira Configuration + +This directory contains configuration files for Jira board and project management. + +## Overview + +This folder tracks API-manageable metadata for Jira entities used by the OpenShift Edge team. Each JSON file contains structured data that can be queried and modified via Jira REST APIs. Configuration elements that require the Jira UI are documented but not stored. + +## Files + +- `boards.json` - Board metadata (ID, name, type, filter, properties) +- `filters.json` - Filter metadata (ID, name, JQL, permissions) +- `projects.json` - Project metadata (ID, key, name, lead, roles) +- `components.json` - Component metadata (name, description, project associations) +- `plans.json` - Advanced Roadmaps plan metadata (ID, title, issue sources, configuration) +- `labels.json` - Team-specific label definitions and their purposes + +## Current Configuration + +### Boards + +- **OpenShift Edge Scrum** (11479) - Scrum board for main team workflow +- **OCPEDGE Scrum** (8557) - Legacy scrum board +- **OpenShift Edge RHEL Verification Tickets** (11551) - Kanban board for RHEL verification + +### Filters + +**Workstream Filters:** + +- OpenShift Edge Workstream - Adaptable Topology (105160) +- OpenShift Edge Workstream - SNO (105147) +- OpenShift Edge Workstream - TNA (105141) +- OpenShift Edge Workstream - TNF (105142) +- OpenShift Edge Workstream - MicroShift (105139) +- OpenShift Edge Workstream - MicroShift to SNO (105161) +- OpenShift Edge Workstream - LVMS (105140) +- OpenShift Edge Workstream - Bandwidth Reduction (105164) +- OpenShift Edge Workstream - RHEL Ticket Verification (105719) + +**Utility Filters:** + +- OpenShift Edge - Core Backlog (104868) +- OpenShift Edge - Bugs and CVEs (104860) +- OpenShift Edge - Components (104874) +- OpenShift Edge - Labels (104872) +- OpenShift Edge - Team Assigned (104844) +- OpenShift Edge - QE Assigned (104857) +- OpenShift Edge - External Projects (104866) +- OpenShift Edge - Scrum Board (104882) + +### Projects + +- **OCPEDGE** (11690) - OpenShift Edge Enablement + +### Components + +- Microshift +- Topology Transition +- Bandwidth Reduction +- Logical Volume Manager Storage +- Planning +- SNO +- TNF (Two Node with Fencing) +- TNA (Two Node with Arbiter) + +### Plans + +- **OpenShift Edge Unified Plan** (1379) - Advanced Roadmaps plan using filter 104341 + +### Labels + +- **OCPEDGE:Docs** - Docs specific tasks +- **OCPEDGE:QE** - QE specific tasks +- **OCPEDGE:RHEL-Verification** - RHEL Verification tasks for TNF +- **OCPEDGE:CI** - CI bugs and automation tasks +- **OCPEDGE:Payload-Manager** - Payload manager duties and automation +- **OCPEDGE:Tooling** - Team tooling improvements +- **OCPEDGE:Process-Improvement** - Process investigation and improvements + +## Board Configuration Details + +### OpenShift Edge Scrum (Board 11479) + +**Properties:** + +- `jsw-roadmaps-classic-board-enable-roadmaps`: `true` - Roadmaps feature enabled +- `jsw-roadmaps-cmp-enable-child-issue-planning`: `true` - Child issue planning enabled + +**Board Features:** + +- **Type**: Scrum +- **Project**: OCPEDGE (OpenShift Edge) +- **Columns**: To Do → In Progress → Tech Review → QE Review → Done +- **Estimation**: Story Points (customfield_10028) +- **Ranking**: Custom field 10019 +- **Constraint Type**: None + +## Board Management + +### API Capabilities + +**Read Operations:** + +- List boards +- Get board details +- Get board configuration (columns, estimation, ranking) +- Get/list board properties + +**Write Operations:** + +- Create board +- Delete board +- Set/delete board properties (custom metadata) + +### UI-Only Configuration + +**Board configuration modifications require the Jira web interface:** + +- Column mappings and status assignments +- Estimation field configuration +- Ranking field settings +- Quick filters +- Card layout and colors +- Swimlane configuration +- Working days and time tracking + +**Note:** While board properties (custom metadata) are writable via API, structural configuration elements are read-only and must be modified through the Jira UI. + +## Filter Management + +### API Capabilities + +**Read Operations:** + +- List filters +- Get filter details + +**Write Operations:** + +- Create filter (name, description, JQL, permissions) +- Update filter (name, description, JQL) +- Delete filter +- Update edit/share permissions + +### UI-Only Configuration + +**Filter ownership transfers** - Must use Jira UI to change filter owner + +## Project Management + +### API Capabilities + +**Read Operations:** + +- List projects +- Get project details + +**Write Operations:** + +- Create project (key, name, description, lead, assigneeType) +- Update project (name, description, lead, assigneeType) +- Delete project + +### UI-Only Configuration + +**Project configuration:** + +- Issue types and workflows +- Custom fields +- Screens and field configurations +- Permissions and roles +- Versions and releases + +**Note:** Components are managed via separate API endpoints (see Components section) + +## Component Management + +### API Capabilities + +**Read Operations:** + +- List project components +- Get component details + +**Write Operations:** + +- Create component (name, description, project, lead, assignee) +- Update component (name, description, lead, assignee) +- Delete component + +## Plan Management + +### API Capabilities + +**Read Operations:** + +- Get plan details (via `/rest/jpo/1.0/plans/{planId}`) +- List issue sources and calculation configuration + +**Write Operations:** + +Plans (Advanced Roadmaps) have limited API support. Most plan configuration requires the Jira UI. + +### UI-Only Configuration + +**Plan configuration:** + +- Issue source filters +- Planning unit (Days/Weeks/Months) +- Non-working days +- Calculation settings (sprints, teams, releases) +- Date field mappings +- Completed issue retention + +## Label Management + +Labels in Jira are lightweight tags without formal metadata or API endpoints. The `labels.json` file serves as team documentation for label naming conventions and purposes. + +**Note:** Labels are applied directly to issues via the standard issue update API. There is no separate label management API. + +## REST API Endpoints + +```bash +# Get board configuration (read-only) +GET /rest/agile/1.0/board/{boardId}/configuration + +# List board properties +GET /rest/agile/1.0/board/{boardId}/properties + +# Set board property +PUT /rest/agile/1.0/board/{boardId}/properties/{propertyKey} +``` diff --git a/edge-scrum/.jira-config/boards.json b/edge-scrum/.jira-config/boards.json new file mode 100644 index 00000000..1bf931a2 --- /dev/null +++ b/edge-scrum/.jira-config/boards.json @@ -0,0 +1,31 @@ +{ + "boards": [ + { + "id": "11479", + "name": "OpenShift Edge Scrum", + "type": "scrum", + "filter_id": "104882", + "properties": { + "jsw-roadmaps-classic-board-enable-roadmaps": true, + "jsw-roadmaps-cmp-enable-child-issue-planning": true + } + }, + { + "id": "8557", + "name": "OCPEDGE Scrum", + "type": "scrum", + "filter_id": "77276", + "properties": {} + }, + { + "id": "11551", + "name": "OpenShift Edge RHEL Verification Tickets", + "type": "kanban", + "filter_id": "105719", + "properties": { + "jsw-roadmaps-classic-board-enable-roadmaps": false, + "jsw-roadmaps-cmp-enable-child-issue-planning": true + } + } + ] +} diff --git a/edge-scrum/.jira-config/components.json b/edge-scrum/.jira-config/components.json new file mode 100644 index 00000000..998fcfc1 --- /dev/null +++ b/edge-scrum/.jira-config/components.json @@ -0,0 +1,91 @@ +{ + "components": [ + { + "name": "Microshift", + "description": "For capturing work items that fall under the Microshift workstream [gitops-managed]", + "projects": [ + { + "project_id": "11690", + "project_key": "OCPEDGE" + } + ] + }, + { + "name": "Topology Transition", + "description": "For capturing work items that fall under the Topology Transition workstream [gitops-managed]", + "projects": [ + { + "component_id": "83238", + "project_id": "11690", + "project_key": "OCPEDGE" + } + ] + }, + { + "name": "Bandwidth Reduction", + "description": "For capturing work items that fall under the Bandwidth Reduction workstream [gitops-managed]", + "projects": [ + { + "component_id": "83240", + "project_id": "11690", + "project_key": "OCPEDGE" + } + ] + }, + { + "name": "Logical Volume Manager Storage", + "description": "For capturing work items that fall under the LVMS workstream [gitops-managed]", + "projects": [ + { + "component_id": "64858", + "project_id": "11690", + "project_key": "OCPEDGE" + } + ] + }, + { + "name": "Planning", + "description": "Used for cutline epics only. This should not be used for anything else. [gitops-managed]", + "projects": [ + { + "component_id": "64855", + "project_id": "11690", + "project_key": "OCPEDGE" + } + ] + }, + { + "name": "SNO", + "description": "For capturing work items that fall under the LVMS workstream [gitops-managed]", + "projects": [ + { + "component_id": "64850", + "project_id": "11690", + "project_key": "OCPEDGE" + } + ] + }, + { + "name": "TNF", + "description": "For capturing work items that fall under the Two Node with Fencing workstream [gitops-managed]", + "projects": [ + { + "component_id": "64861", + "project_id": "11690", + "project_key": "OCPEDGE" + } + ] + }, + { + "name": "TNA", + "description": "For capturing work items that fall under the Two Node with Arbiter workstream [gitops-managed]", + "projects": [ + { + "component_id": "64853", + "project_id": "11690", + "project_key": "OCPEDGE" + } + ] + } + ] +} diff --git a/edge-scrum/.jira-config/filters.json b/edge-scrum/.jira-config/filters.json new file mode 100644 index 00000000..cb3681a0 --- /dev/null +++ b/edge-scrum/.jira-config/filters.json @@ -0,0 +1,378 @@ +{ + "filters": [ + { + "id": "105160", + "name": "OpenShift Edge Workstream - Adaptable Topology", + "description": "An OpenShift Edge utility filter to limit lists specifically to Adaptable Topology work", + "jql": "component = \"Adaptable Topology\" or labels in (adaptable-topology)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "105147", + "name": "OpenShift Edge Workstream - SNO", + "description": "An OpenShift Edge utility filter to limit lists specifically to SNO work", + "jql": "component in (\"Installer / Single Node OpenShift\", SNO) or labels in (SNO, sno)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "105141", + "name": "OpenShift Edge Workstream - TNA", + "description": "An OpenShift Edge utility filter to limit lists specifically to Two Node with Arbiter work", + "jql": "component = \"Two Node with Arbiter\" or labels in (TNA)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "104860", + "name": "OpenShift Edge - Bugs and CVEs", + "description": "Bugs and CVEs that affect any of the work streams in the OpenShift Edge organization", + "jql": "(project = OCPBUGS AND filter = \"OpenShift Edge - Components\") OR (filter = \"OpenShift Edge - Core Backlog\" AND type in (Bug, Vulnerability, Weakness)) OR (project = TRT and filter = \"OpenShift Edge - Team Assigned\")", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "104868", + "name": "OpenShift Edge - Core Backlog", + "description": "All issues from the OCPEDGE and USHIFT projects", + "jql": "project in (OCPEDGE, USHIFT)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "105142", + "name": "OpenShift Edge Workstream - TNF", + "description": "", + "jql": "component = \"Two Node Fencing\" or labels in (TNF)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "105139", + "name": "OpenShift Edge Workstream - MicroShift", + "description": "", + "jql": "project = USHIFT or component in (MicroShift, \"MicroShift / Networking\", \"MicroShift / Storage\") OR labels in (microshift, MicroShift)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "104872", + "name": "OpenShift Edge - Labels", + "description": "Any labels that might be relevant to Openshift Edge", + "jql": "labels in (ocpedge, ocpedge-plan, MicroShift, microshift, TNF, TNA, SNO, sno, lvms, edge-payload)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "105161", + "name": "OpenShift Edge Workstream - MicroShift to SNO", + "description": "", + "jql": "component = \"MicroShift to SNO\" or labels in (microshift-to-sno)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "104874", + "name": "OpenShift Edge - Components", + "description": "Any components that might be relevant to Openshift Edge", + "jql": "component in (\"Installer / Single Node OpenShift\", \"Two Node with Arbiter\", \"Two Node Fencing\", \"Logical Volume Manager Storage\", MicroShift, \"MicroShift / Networking\", \"MicroShift / Storage\")", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "104882", + "name": "OpenShift Edge - Scrum Board", + "description": "", + "jql": "project in (USHIFT, OCPEDGE) OR filter = \"OpenShift Edge - Core Backlog\" OR filter in (\"OpenShift Edge - External Projects\", \"OpenShift Edge - Bugs and CVEs\") AND (filter = \"OpenShift Edge - Team Assigned\" OR (filter = \"OpenShift Edge - QE Assigned\" AND status in (Review, MODIFIED, ON_QA, Testing))) ORDER BY Rank ASC", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "104844", + "name": "OpenShift Edge - Team Assigned", + "description": "A filter for limiting issue searches to issues assigned to OpenShift Edge Members", + "jql": "assignee IN (712020:776c46bf-4843-415f-b22c-2cfe1a0dbf76, 712020:8ccae1f4-8b38-483e-b8fb-803381ed8128, 712020:eca54813-6101-4bb6-b169-5b91a02029a1, 70121:fd715ae6-c353-45e3-ab6d-4d856939baac, 712020:f55cd58b-0b6b-4f4c-bab6-4b6d159fa903, 712020:603765f6-a123-4102-81eb-35016a2417dc, 712020:2fe6ed01-1363-498c-b60c-47aaa73c31b8, 712020:3f5c0d77-9c08-4283-b8d4-a8c36239f0cc, 712020:68a47ba1-67a5-49a1-b22f-67e305d3ce13, 712020:99131988-93fb-4f73-9049-65977ee89263, 712020:accacd89-ddc5-46bf-852d-6b81923e7f10, 60213e1561800100699b61a0, 5c9fb65763c4be37069fc169, 61e811135fcc37006876d93a, 70121:978d797b-a96a-43b1-a1bc-aad7c053f205, 61a8ef1cc15977006aed294c, 712020:188ebd21-393a-42a0-82dc-4646bc713e61, 712020:a2a53ec9-624d-43dd-b9e9-2631248317b1, 70121:b3364a04-e4bd-41d1-bae9-80e0d45657b0, 712020:f96735f0-ab26-4108-b61f-236febb8e20b)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "104857", + "name": "OpenShift Edge - QE Assigned", + "description": "A filter for limiting issue searches to issues OpenShift Edge Members as the assigned QA Contact", + "jql": "\"QA Contact\" IN (712020:776c46bf-4843-415f-b22c-2cfe1a0dbf76, 712020:8ccae1f4-8b38-483e-b8fb-803381ed8128, 712020:eca54813-6101-4bb6-b169-5b91a02029a1, 70121:fd715ae6-c353-45e3-ab6d-4d856939baac, 712020:f55cd58b-0b6b-4f4c-bab6-4b6d159fa903, 712020:603765f6-a123-4102-81eb-35016a2417dc, 712020:2fe6ed01-1363-498c-b60c-47aaa73c31b8, 712020:3f5c0d77-9c08-4283-b8d4-a8c36239f0cc, 712020:68a47ba1-67a5-49a1-b22f-67e305d3ce13, 712020:99131988-93fb-4f73-9049-65977ee89263, 712020:accacd89-ddc5-46bf-852d-6b81923e7f10, 60213e1561800100699b61a0, 5c9fb65763c4be37069fc169, 61e811135fcc37006876d93a, 70121:978d797b-a96a-43b1-a1bc-aad7c053f205, 61a8ef1cc15977006aed294c, 712020:188ebd21-393a-42a0-82dc-4646bc713e61, 712020:a2a53ec9-624d-43dd-b9e9-2631248317b1, 70121:b3364a04-e4bd-41d1-bae9-80e0d45657b0, 712020:f96735f0-ab26-4108-b61f-236febb8e20b)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "105719", + "name": "OpenShift Edge Workstream - RHEL Ticket Verification", + "description": "", + "jql": "(project = RHEL AND summary ~ \"\\\\[TNF\\\\]\" AND component = resource-agents) OR (project = OCPEDGE AND component = RHEL-Verification) order by RANK asc", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "105140", + "name": "OpenShift Edge Workstream - LVMS", + "description": "", + "jql": "component = \"Logical Volume Manager Storage\" or labels in (lvms)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "104866", + "name": "OpenShift Edge - External Projects", + "description": "External Projects that OpenShift Edge team members might be contributing to", + "jql": "project in (\"Cloud-native Network Functions\", \"Network Edge\", \"OpenShift Staff Engineering\", \"OCP Technical Release Team\", \"On Prem Networking\", \"Edge and Ecosystem Enablement\", \"OpenShift Core Networking\", API, ETCD, OSDOCS, PERFSCALE, \"Operator Runtime\", \"Distributed Tracing\") AND filter in (\"OpenShift Edge - Components\", \"OpenShift Edge - Labels\", \"OpenShift Edge - QE Assigned\", \"OpenShift Edge - Team Assigned\")", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + }, + { + "id": "105164", + "name": "OpenShift Edge Workstream - Bandwidth Reduction", + "description": "", + "jql": "component = \"Bandwidth Reduction\" or labels in (bandwidth-reduction)", + "editPermissions": [ + { + "type": "group", + "group": { + "name": "openshift-scrummaster-role" + } + } + ], + "sharePermissions": [ + { + "type": "group", + "group": { + "name": "Red Hat Employee" + } + } + ] + } + ] +} diff --git a/edge-scrum/.jira-config/labels.json b/edge-scrum/.jira-config/labels.json new file mode 100644 index 00000000..62780002 --- /dev/null +++ b/edge-scrum/.jira-config/labels.json @@ -0,0 +1,32 @@ +{ + "labels": [ + { + "name": "OCPEDGE:Docs", + "description": "Docs specific tasks for OpenShift Edge" + }, + { + "name": "OCPEDGE:QE", + "description": "QE specific tasks for OpenShift Edge" + }, + { + "name": "OCPEDGE:RHEL-Verification", + "description": "OpenShift Edge tracker label for tickets related to RHEL Verification tasks for TNF" + }, + { + "name": "OCPEDGE:CI", + "description": "For tracking any bugs specific to OpenShift Edge CI or for CI automation tasks" + }, + { + "name": "OCPEDGE:Payload-Manager", + "description": "For capturing work done as part of payload manager duties and Payload Manager Duty automation" + }, + { + "name": "OCPEDGE:Tooling", + "description": "For capturing work to improve tooling for the OpenShift Edge team (ie: improvements in the edge-tooling repo)" + }, + { + "name": "OCPEDGE:Process-Improvement", + "description": "For capturing work to investigate, document, and implement improvements to processes related to OpenShift Edge" + } + ] +} diff --git a/edge-scrum/.jira-config/projects.json b/edge-scrum/.jira-config/projects.json new file mode 100644 index 00000000..c209edff --- /dev/null +++ b/edge-scrum/.jira-config/projects.json @@ -0,0 +1,156 @@ +{ + "projects": [ + { + "id": "11690", + "key": "OCPEDGE", + "name": "OpenShift Edge Enablement", + "description": "Project for the OpenShift Edge Team", + "assigneeType": "UNASSIGNED", + "isPrivate": false, + "lead": { + "id": "712020:93a6a685-d36f-4660-a64a-6870675c2ec8", + "email": "cscribne@redhat.com" + }, + "roles": [ + { + "id": "10002", + "name": "Administrators", + "description": "A project role that represents administrators in a project", + "actors": [ + { + "type": "atlassian-user-role-actor", + "account_id": "712020:93a6a685-d36f-4660-a64a-6870675c2ec8", + "display_name": "Chad Scribner" + }, + { + "type": "atlassian-user-role-actor", + "account_id": "712020:eca54813-6101-4bb6-b169-5b91a02029a1", + "display_name": "Jeff Roche" + }, + { + "type": "atlassian-user-role-actor", + "account_id": "70121:fd715ae6-c353-45e3-ab6d-4d856939baac", + "display_name": "Jeremy Poulin" + }, + { + "type": "atlassian-user-role-actor", + "account_id": "712020:188ebd21-393a-42a0-82dc-4646bc713e61", + "display_name": "Pablo Acevedo Montserrat" + }, + { + "type": "atlassian-user-role-actor", + "account_id": "61e811135fcc37006876d93a", + "display_name": "Gregory Giguashvili" + }, + { + "type": "atlassian-user-role-actor", + "account_id": "604a3e8543eac6006f8fbe82", + "display_name": "Nicole Wilker" + }, + { + "type": "atlassian-user-role-actor", + "account_id": "604bf97ce19f910069714b6c", + "display_name": "Sarah Kyros" + }, + { + "type": "atlassian-user-role-actor", + "account_id": "712020:06373c2b-9dbb-4310-b3c9-05b75997831c", + "display_name": "Suleyman Akbas" + }, + { + "type": "atlassian-group-role-actor", + "group_id": "21adff1a-e3a7-434f-a9e0-ad57c4f70762", + "group_name": "jira-administrators" + } + ] + }, + { + "id": "10125", + "name": "Users", + "description": "Standard user access", + "actors": [] + }, + { + "id": "10126", + "name": "Developers", + "description": "This project role is intended for anyone who is a team member, actively working in the Jira project. Developers cannot access the project settings, comments, and attachments that are not their own. They also do not have the ability to manage components, releases, or sprint information in the project. Developers do have the ability to start and complete sprints.", + "actors": [ + { + "type": "atlassian-user-role-actor", + "account_id": "712020:07290b97-3f2b-4fe2-81a5-d5d501f306fe", + "display_name": "Edge JiraBot" + }, + { + "type": "atlassian-user-role-actor", + "account_id": "712020:510de16d-fc42-48cf-8412-8b5cb72dc6bd", + "display_name": "Jira-SD-Elements-Integration Bot" + }, + { + "type": "atlassian-group-role-actor", + "group_id": "70b1c436-6bfe-4027-bc2d-78ec147d8cb1", + "group_name": "OpenShift Jira Bots" + }, + { + "type": "atlassian-group-role-actor", + "group_id": "13bd0387-d75c-4c18-9d37-5439e8bf984c", + "group_name": "Red Hat Employee" + } + ] + }, + { + "id": "10127", + "name": "Scrum master", + "description": "This role is intended for users that assist with day-to-day operation of the team but don't require full project administration access. Scrum masters, program managers, and others may find themselves in this role. It provides access to create, edit, start, and end sprints.", + "actors": [ + { + "type": "atlassian-user-role-actor", + "account_id": "712020:eca54813-6101-4bb6-b169-5b91a02029a1", + "display_name": "Jeff Roche" + }, + { + "type": "atlassian-group-role-actor", + "group_id": "4937a28d-eae0-4776-990c-589b162a7fe7", + "group_name": "openshift-scrummaster-role" + } + ] + }, + { + "id": "10128", + "name": "Approver", + "description": "Approval permissions", + "actors": [] + }, + { + "id": "10129", + "name": "Viewers", + "description": "Read-only access", + "actors": [] + }, + { + "id": "10162", + "name": "Version Manager", + "description": "Manages versions and releases", + "actors": [] + }, + { + "id": "10056", + "name": "Service Desk Customers", + "description": "Service desk customer access", + "actors": [] + }, + { + "id": "10057", + "name": "Service Desk Team", + "description": "Service desk team members", + "actors": [] + }, + { + "id": "10003", + "name": "atlassian-addons-project-access", + "description": "Atlassian add-on access", + "actors": [] + } + ] + } + ] +} diff --git a/edge-scrum/README.md b/edge-scrum/README.md new file mode 100644 index 00000000..07201205 --- /dev/null +++ b/edge-scrum/README.md @@ -0,0 +1,121 @@ +# OpenShift Edge Scrum Tooling + +## Jira Configuration Management + +Jira metadata (boards, filters, projects, components, labels) is managed as code in `.jira-config/` and synced to Jira automatically via CI. + +### How it works + +1. Edit the relevant JSON file in `.jira-config/` +2. Update `.jira-config/README.md` to reflect the change (required by CI) +3. Open a PR — CI validates the JSON schema and previews what would be synced +4. On merge to `main`, changes are applied to Jira automatically + +> **Note:** Create and delete operations are manual. The sync scripts only update existing entities. + +### `.jira-config/` — configuration files + +| File | Manages | +|------|---------| +| `boards.json` | Board properties and roadmap feature flags | +| `filters.json` | Filter name, JQL, and share/edit permissions | +| `projects.json` | Project metadata and lead assignment | +| `components.json` | Component names and descriptions | +| `labels.json` | Team label definitions | + +See `.jira-config/README.md` for the full inventory of managed entities. + +### `.ci/` — automation + +| Path | Purpose | +|------|---------| +| `.ci/scripts/validate-configs.sh` | Validates all `.jira-config` JSON files against their schemas | +| `.ci/scripts/validate-readme-sync.sh` | Enforces that README is updated alongside config changes | +| `.ci/scripts/apply-changes.sh` | Detects which configs changed and routes to the appropriate sync script | +| `.ci/scripts/sync-boards.sh` | Syncs board properties via Jira Agile API | +| `.ci/scripts/sync-filters.sh` | Syncs filter metadata via Jira REST API | +| `.ci/scripts/sync-projects.sh` | Syncs project metadata via Jira REST API | +| `.ci/scripts/sync-components.sh` | Syncs component metadata via Jira REST API | +| `.ci/schemas/` | JSON Schema files used by `validate-configs.sh` | + +### Running locally + +```bash +# Validate config files +edge-scrum/.ci/scripts/validate-configs.sh + +# Preview changes that would be synced (no API calls made) +edge-scrum/.ci/scripts/apply-changes.sh --dry-run + +# Apply changes +edge-scrum/.ci/scripts/apply-changes.sh +``` + +Credentials are read from environment variables. `JIRA_URL` defaults to `https://redhat.atlassian.net`: + +```bash +export JIRA_USERNAME= +export JIRA_API_TOKEN= +``` + +### Manual full sync + +To force-sync all configs regardless of what changed, trigger the **Jira Config — Apply Changes** workflow manually from the GitHub Actions tab (`workflow_dispatch`). + +--- + +## MCP Servers + +### Jira MCP Server + +In order to utilize jira, you will need to have an Atlassian MCP server configured. In order to configure the MCP server, you will need an API token which you can generate from your [Atlassian Account security tab](https://id.atlassian.com/manage-profile/security/api-tokens). + +#### sooperset Atlassian MCP Server Configuration + +```json +{ + "mcpServers": { + "mcp-atlassian": { + "command": "podman", + "args": [ + "run", + "--rm", + "-i", + "-e", + "JIRA_URL", + "-e", + "JIRA_USERNAME", + "-e", + "JIRA_API_TOKEN", + "-e", + "JIRA_SSL_VERIFY", + "-e", + "CONFLUENCE_URL", + "-e", + "CONFLUENCE_USERNAME", + "-e", + "CONFLUENCE_API_TOKEN", + "-e", + "CONFLUENCE_SSL_VERIFY", + "ghcr.io/sooperset/mcp-atlassian:latest" + ], + "env": { + "JIRA_URL": "https://redhat.atlassian.net", + "JIRA_USERNAME": "", + "JIRA_API_TOKEN": "", + "JIRA_SSL_VERIFY": "true", + "CONFLUENCE_URL": "https://redhat.atlassian.net/wiki", + "CONFLUENCE_USERNAME": "", + "CONFLUENCE_API_TOKEN": "", + "CONFLUENCE_SSL_VERIFY": "true" + } + } + } +} +``` + +> **Note:** You can skip the Confluence variables if you don't plan to use the Confluence portion of the MCP server + +#### Atlassian Rovo MCP Server Configuration + +To run the Atlassian Rovo MCP server, follow [their setup instructions](https://support.atlassian.com/atlassian-rovo-mcp-server/docs/getting-started-with-the-atlassian-remote-mcp-server/).