diff --git a/.github/.copilotignore b/.github/.copilotignore new file mode 100644 index 0000000..737d4f6 --- /dev/null +++ b/.github/.copilotignore @@ -0,0 +1 @@ +- content/**/* \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e667bab..a44a8cb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ -# For more information, see [docs](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax) +# For more information, see [docs](https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax) # This repository is maintained by: * @geektrainer @peckjon @chrisreddington diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..bbe6223 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,115 @@ +# Tailspin Shelter + +The Tailspin Shelter is a full-stack web application that showcases a fictional dog shelter. + +## Technology Stack + +### Frontend (Client) + +- **Framework**: [Astro](https://astro.build/) v5.4+ - Static site generator with server-side rendering +- **Component Library**: [Svelte](https://svelte.dev/) v5.23+ - For dynamic interactive components +- **Styling**: [Tailwind CSS](https://tailwindcss.com/) v4.0+ - Utility-first CSS framework +- **Language**: TypeScript - Strongly typed JavaScript +- **Adapter**: Node.js adapter for server-side rendering + +### Backend (Server) + +- **Framework**: [Flask](https://flask.palletsprojects.com/) - Python web framework +- **Database**: SQLite with [SQLAlchemy](https://www.sqlalchemy.org/) ORM +- **Language**: Python 3.13+ with type hints +- **CORS**: Flask-CORS for cross-origin requests + +### Testing + +- **Python/Server/Flask**: Python unittest framework +- **E2E testing**: [Playwright](https://playwright.dev/) v1.49+ - End-to-end testing framework + +## Project Structure + +``` +pets-workshop/ +├── client/ # Astro frontend application +│ ├── src/components/ # Svelte components (DogList, DogDetails) +│ ├── src/layouts/ # Astro layout templates +│ ├── src/pages/ # Astro pages (routing) +│ ├── src/styles/ # Global CSS and Tailwind imports +│ └── e2e-tests/ # Playwright end-to-end tests +├── server/ # Flask backend API +│ ├── models/ # SQLAlchemy models (Dog, Breed) +│ ├── tests/ # Python unit tests +│ └── app.py # Main Flask application +└── scripts/ # Automation scripts (run-tests.sh, setup-environment.sh, start-app.sh) +``` + +## Design Philosophy & Theme + +**CRITICAL**: Maintain the dark, modern aesthetic throughout: +- **HTML Class**: Always include `class="dark"` on the html element +- **Background**: Use `bg-slate-900` for main backgrounds +- **Text**: Default to `text-white` for primary content +- **Typography**: Inter font family with clean, readable text +- **Responsive**: Mobile-first approach using Tailwind's responsive prefixes +- **Transitions**: Include `transition-colors duration-300` for smooth interactions + +## Development Guidelines + +### Use Scripts, Not Direct Commands +**IMPORTANT**: Always prefer using the provided scripts in the `scripts/` directory rather than running commands directly: +- **Testing**: Use `./scripts/run-server-tests.sh` instead of `python -m unittest` +- **E2E Testing**: Use `./scripts/run-e2e-tests` instead of `npm run tests:e2e` +- **Environment Setup**: Use `./scripts/setup-environment.sh` for initial setup +- **Application Start**: Use `./scripts/start-app.sh` to launch the application + +### API Patterns +- **Endpoints**: RESTful API design with `/api/` prefix +- **Response Format**: Always return JSON with proper HTTP status codes +- **Type Hints**: Use Python type hints for all function parameters and returns + +### Frontend Patterns +- **Component Structure**: Use Svelte for interactive components, Astro for static layouts +- **Data Fetching**: Fetch data on the server side when possible +- **Styling**: Use Tailwind utility classes, avoid custom CSS unless necessary +- **Routing**: File-based routing through Astro's pages directory +- **Test Identifiers**: Always include `data-testid` attributes for E2E testing resilience (see [`test-identifiers.md`](./instructions/test-identifiers.md)) + +### Database Patterns +- **Models**: Use SQLAlchemy declarative base with proper relationships +- **Queries**: Prefer SQLAlchemy query syntax over raw SQL +- **Data Seeding**: Use the utilities in `utils/seed_database.py` + +### Testing Patterns + +Below are the only types of tests we use in this project. Do not add additional test types unless instructed otherwise. + +- **E2E Tests**: Playwright tests in `client/e2e-tests/` cover full user workflows +- **Unit tests**: Unit tests for Flask endpoints and utilities, stored in `server/tests` + +## Coding Standards + +### Python (Backend) +- Follow PEP 8 style guidelines +- Use type hints for all function signatures +- Use meaningful variable names with snake_case +- Handle exceptions gracefully with proper error messages + +### TypeScript/JavaScript (Frontend) +- Use TypeScript for type safety +- Follow Astro's component conventions +- Use camelCase for variables and functions +- Include proper prop types for Svelte components + +### CSS/Styling +- Use Tailwind utility classes primarily +- Maintain dark theme consistency +- Ensure responsive design across all breakpoints +- Use semantic HTML elements when possible + +## AI Assistant Guidelines + +When working with this codebase: +1. Always maintain the dark theme aesthetic +2. Use the provided scripts for common operations +3. Follow the established patterns for API responses and component structure +4. Utilize the tests to validate app behavior; don't launch the app or run `curl` commands to do so +4. Ensure type safety in both Python and TypeScript code +5. Test changes using the appropriate testing frameworks diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fc96c08..ac05a2e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ # - Checks for updates weekly # - Groups updates based on their type (dev grouped by minor/patch or prod grouped by patch) # -# Learn more at https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#configuration-options-for-the-dependabotyml-file +# Learn more at https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#configuration-options-for-the-dependabotyml-file version: 2 updates: - package-ecosystem: npm diff --git a/.github/instructions/flask-tests.instructions.md b/.github/instructions/flask-tests.instructions.md new file mode 100644 index 0000000..020308a --- /dev/null +++ b/.github/instructions/flask-tests.instructions.md @@ -0,0 +1,215 @@ +--- +applyTo: "**/test_*.py" +--- + +# Flask Testing Guidelines for Tailspin Shelter + +Essential patterns for writing unit tests for Flask APIs using Python's unittest framework. + +## Core Principles + +1. **Mock Database Queries** - Use `unittest.mock` to isolate API logic +2. **Test HTTP Responses** - Verify status codes, JSON structure, error messages +3. **Helper Methods** - Create reusable mock objects and setup methods + +## Basic Test Structure + +```python +import unittest +from unittest.mock import patch, MagicMock +import json +from app import app + +class TestDogAPI(unittest.TestCase): + def setUp(self): + self.app = app.test_client() + self.app.testing = True + app.config['TESTING'] = True +``` + +## Testing GET Endpoints + +### List Endpoint +```python +@patch('app.db.session.query') +def test_get_dogs_success(self, mock_query): + # Setup mock + mock_dog = MagicMock() + mock_dog.id = 1 + mock_dog.name = "Buddy" + mock_dog.breed = "Golden Retriever" + + mock_query_instance = MagicMock() + mock_query.return_value = mock_query_instance + mock_query_instance.join.return_value = mock_query_instance + mock_query_instance.all.return_value = [mock_dog] + + response = self.app.get('/api/dogs') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(len(data), 1) + self.assertEqual(data[0]['name'], "Buddy") +``` + +### Single Resource +```python +@patch('app.db.session.query') +def test_get_dog_by_id_success(self, mock_query): + mock_dog = MagicMock() + mock_dog.id = 1 + mock_dog.name = "Buddy" + mock_dog.status.name = "AVAILABLE" + + mock_query_instance = MagicMock() + mock_query.return_value = mock_query_instance + mock_query_instance.join.return_value = mock_query_instance + mock_query_instance.filter.return_value = mock_query_instance + mock_query_instance.first.return_value = mock_dog + + response = self.app.get('/api/dogs/1') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(data['name'], "Buddy") + +@patch('app.db.session.query') +def test_get_dog_not_found(self, mock_query): + mock_query_instance = MagicMock() + mock_query.return_value = mock_query_instance + mock_query_instance.join.return_value = mock_query_instance + mock_query_instance.filter.return_value = mock_query_instance + mock_query_instance.first.return_value = None + + response = self.app.get('/api/dogs/999') + + self.assertEqual(response.status_code, 404) + data = json.loads(response.data) + self.assertEqual(data['error'], "Dog not found") +``` + +## Testing POST Endpoints + +```python +@patch('app.db.session') +@patch('app.Dog') +def test_create_dog_success(self, mock_dog_class, mock_session): + mock_dog_instance = MagicMock() + mock_dog_instance.id = 1 + mock_dog_instance.name = "New Dog" + mock_dog_class.return_value = mock_dog_instance + + dog_data = {'name': 'New Dog', 'breed_id': 1, 'age': 2} + + response = self.app.post('/api/dogs', + data=json.dumps(dog_data), + content_type='application/json') + + self.assertEqual(response.status_code, 201) + data = json.loads(response.data) + self.assertEqual(data['name'], "New Dog") + mock_session.add.assert_called_once() + mock_session.commit.assert_called_once() + +def test_create_dog_missing_fields(self): + dog_data = {'name': 'Incomplete Dog'} # Missing breed_id + + response = self.app.post('/api/dogs', + data=json.dumps(dog_data), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + data = json.loads(response.data) + self.assertIn('Missing required fields', data['error']) +``` + +## Testing PUT/DELETE Endpoints + +```python +@patch('app.Dog.query') +@patch('app.db.session') +def test_update_dog_success(self, mock_session, mock_query): + mock_dog = MagicMock() + mock_dog.id = 1 + mock_query.get.return_value = mock_dog + + update_data = {'name': 'Updated Name', 'age': 4} + + response = self.app.put('/api/dogs/1', + data=json.dumps(update_data), + content_type='application/json') + + self.assertEqual(response.status_code, 200) + self.assertEqual(mock_dog.name, "Updated Name") + mock_session.commit.assert_called_once() + +@patch('app.Dog.query') +@patch('app.db.session') +def test_delete_dog_success(self, mock_session, mock_query): + mock_dog = MagicMock() + mock_query.get.return_value = mock_dog + + response = self.app.delete('/api/dogs/1') + + self.assertEqual(response.status_code, 200) + mock_session.delete.assert_called_once_with(mock_dog) + mock_session.commit.assert_called_once() +``` + +## Error Handling Tests + +```python +@patch('app.db.session') +def test_database_error_handling(self, mock_session): + mock_session.commit.side_effect = Exception("Database error") + + dog_data = {'name': 'Test Dog', 'breed_id': 1} + + response = self.app.post('/api/dogs', + data=json.dumps(dog_data), + content_type='application/json') + + self.assertEqual(response.status_code, 500) + data = json.loads(response.data) + self.assertIn('Internal server error', data['error']) + mock_session.rollback.assert_called_once() +``` + +## Helper Methods + +```python +def _create_mock_dog(self, dog_id: int, name: str, breed: str): + mock_dog = MagicMock() + mock_dog.id = dog_id + mock_dog.name = name + mock_dog.breed = breed + mock_dog.status.name = "AVAILABLE" + return mock_dog + +def _setup_query_mock(self, mock_query, return_data): + mock_query_instance = MagicMock() + mock_query.return_value = mock_query_instance + mock_query_instance.join.return_value = mock_query_instance + mock_query_instance.filter.return_value = mock_query_instance + mock_query_instance.all.return_value = return_data + mock_query_instance.first.return_value = return_data[0] if return_data else None + return mock_query_instance +``` + +## Running Tests + +```bash +# Run all tests +python -m unittest discover tests/ + +# Run with project script +./scripts/run-server-tests.sh +``` + +## Key Testing Patterns + +- **Mock database operations** to isolate API logic +- **Test both success and error cases** for each endpoint +- **Verify HTTP status codes** and response JSON structure +- **Use helper methods** to reduce code duplication +- **Assert database operations** like `add()`, `commit()`, `rollback()` diff --git a/.github/instructions/flask.instructions.md b/.github/instructions/flask.instructions.md new file mode 100644 index 0000000..87d8fa8 --- /dev/null +++ b/.github/instructions/flask.instructions.md @@ -0,0 +1,198 @@ +--- +applyTo: "**/app.py" +--- + +# Flask API Guidelines for Tailspin Shelter + +Essential patterns for writing Flask endpoints with SQLAlchemy in the Tailspin Shelter backend. + +## Core Principles + +1. **Type Hints** - Use Python type hints for all functions +2. **RESTful Design** - Follow `/api/` prefix conventions +3. **Error Handling** - Return proper HTTP status codes with JSON +4. **SQLAlchemy ORM** - Use ORM queries, avoid raw SQL + +## Basic Structure + +```python +from typing import Dict, List, Any +from flask import Flask, jsonify, Response, request +from models import db, Dog, Breed + +@app.route('/api/resource', methods=['GET']) +def get_resource() -> Response: + try: + results = db.session.query(Model).all() + data: List[Dict[str, Any]] = [ + {'id': item.id, 'name': item.name} for item in results + ] + return jsonify(data) + except Exception as e: + return jsonify({"error": f"Internal server error: {str(e)}"}), 500 +``` + +## GET Endpoints + +### List Resources +```python +@app.route('/api/dogs', methods=['GET']) +def get_dogs() -> Response: + query = db.session.query( + Dog.id, Dog.name, Breed.name.label('breed') + ).join(Breed, Dog.breed_id == Breed.id) + + dogs_list: List[Dict[str, Any]] = [ + {'id': dog.id, 'name': dog.name, 'breed': dog.breed} + for dog in query.all() + ] + return jsonify(dogs_list) +``` + +### Single Resource +```python +@app.route('/api/dogs/', methods=['GET']) +def get_dog(id: int) -> tuple[Response, int] | Response: + dog_query = db.session.query( + Dog.id, Dog.name, Breed.name.label('breed'), + Dog.age, Dog.description, Dog.status + ).join(Breed, Dog.breed_id == Breed.id).filter(Dog.id == id).first() + + if not dog_query: + return jsonify({"error": "Dog not found"}), 404 + + return jsonify({ + 'id': dog_query.id, + 'name': dog_query.name, + 'breed': dog_query.breed, + 'age': dog_query.age, + 'status': dog_query.status.name + }) +``` + +## POST - Create Resource +```python +@app.route('/api/dogs', methods=['POST']) +def create_dog() -> tuple[Response, int] | Response: + try: + data = request.get_json() + + if not data or 'name' not in data or 'breed_id' not in data: + return jsonify({"error": "Missing required fields: name, breed_id"}), 400 + + new_dog = Dog( + name=data['name'], + breed_id=data['breed_id'], + age=data.get('age'), + description=data.get('description') + ) + + db.session.add(new_dog) + db.session.commit() + + return jsonify({ + 'id': new_dog.id, + 'name': new_dog.name, + 'message': 'Dog created successfully' + }), 201 + + except ValueError as e: + db.session.rollback() + return jsonify({"error": f"Validation error: {str(e)}"}), 400 + except Exception as e: + db.session.rollback() + return jsonify({"error": "Internal server error"}), 500 +``` + +## PUT - Update Resource +```python +@app.route('/api/dogs/', methods=['PUT']) +def update_dog(id: int) -> tuple[Response, int] | Response: + try: + dog = Dog.query.get(id) + if not dog: + return jsonify({"error": "Dog not found"}), 404 + + data = request.get_json() + if not data: + return jsonify({"error": "No data provided"}), 400 + + # Update fields if provided + for field in ['name', 'age', 'description']: + if field in data: + setattr(dog, field, data[field]) + + db.session.commit() + return jsonify({'id': dog.id, 'message': 'Dog updated successfully'}) + + except Exception as e: + db.session.rollback() + return jsonify({"error": "Internal server error"}), 500 +``` + +## DELETE - Remove Resource +```python +@app.route('/api/dogs/', methods=['DELETE']) +def delete_dog(id: int) -> tuple[Response, int] | Response: + try: + dog = Dog.query.get(id) + if not dog: + return jsonify({"error": "Dog not found"}), 404 + + db.session.delete(dog) + db.session.commit() + return jsonify({"message": "Dog deleted successfully"}) + + except Exception as e: + db.session.rollback() + return jsonify({"error": "Internal server error"}), 500 +``` + +## Common Patterns + +### Query Examples +```python +# Join query +query = db.session.query(Dog.id, Breed.name.label('breed')).join(Breed) + +# Filtered query +dogs = Dog.query.filter(Dog.status == AdoptionStatus.AVAILABLE).all() + +# Pagination +page = request.args.get('page', 1, type=int) +dogs = Dog.query.paginate(page=page, per_page=10, error_out=False) +``` + +### Input Validation +```python +def validate_dog_data(data: Dict[str, Any]) -> Optional[str]: + if not data.get('name') or len(data['name'].strip()) < 2: + return "Name must be at least 2 characters" + if 'age' in data and (not isinstance(data['age'], int) or data['age'] < 0): + return "Age must be a positive integer" + return None +``` + +### Error Responses +```python +# Standard status codes +return jsonify({"error": "Invalid input"}), 400 # Bad Request +return jsonify({"error": "Not found"}), 404 # Not Found +return jsonify({"error": "Internal error"}), 500 # Server Error + +# Transaction safety +try: + db.session.commit() +except Exception: + db.session.rollback() + return jsonify({"error": "Operation failed"}), 500 +``` + +## Key Patterns + +- **Type hints** for all function parameters and returns +- **JSON responses** with `jsonify()` and proper status codes +- **Error handling** with try/catch and rollback on failures +- **Input validation** before database operations +- **RESTful URLs** following `/api/resource` patterns +- **SQLAlchemy ORM** with joins and filters, not raw SQL diff --git a/.github/instructions/playwright.instructions.md b/.github/instructions/playwright.instructions.md new file mode 100644 index 0000000..36fff7e --- /dev/null +++ b/.github/instructions/playwright.instructions.md @@ -0,0 +1,158 @@ +--- +applyTo: "**/*.spec.ts" +--- + +# Playwright E2E Testing Guidelines for Tailspin Shelter + +Essential patterns for writing effective Playwright tests in the Tailspin Shelter application. + +## Core Principles + +1. **Test User Workflows** - Focus on complete user journeys, not implementation details +2. **Use Test IDs First** - Always prefer `data-testid` attributes for reliable element identification +3. **Semantic Locators Second** - Use `getByRole()`, `getByText()`, `getByLabel()` when test IDs aren't available +4. **Handle Async Behavior** - Always account for loading states and API calls + +> 📋 **Reference**: See [`test-identifiers.md`](./test-identifiers.md) for complete list of available test IDs + +## Locator Patterns + +### Preferred Approach (In Order of Priority) +```typescript +// ✅ Test IDs (most reliable) +await page.getByTestId('dog-card-1').click(); +await expect(page.getByTestId('homepage-title')).toBeVisible(); +await page.getByTestId('back-to-dogs-button').click(); + +// ✅ Semantic locators (when test IDs aren't available) +await page.getByRole('heading', { name: 'Welcome to Tailspin Shelter' }).click(); +await page.getByRole('link', { name: 'Back to All Dogs' }).click(); + +// ✅ Combined approach (test ID + semantic validation) +const dogCard = page.getByTestId('dog-card-1'); +await expect(dogCard.getByTestId('dog-name-1')).toContainText('Buddy'); +await dogCard.click(); +``` + +### Avoid +```typescript +// ❌ Fragile CSS selectors +await page.locator('.bg-slate-800 .p-6 h3').click(); +await page.locator('a').nth(0).click(); +await page.locator('.grid > div:first-child').click(); +``` + +## Essential Test Patterns + +### Basic Test Structure +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('Feature Name', () => { + test('should perform user action', async ({ page }) => { + await page.goto('/'); + await page.getByRole('button', { name: 'Submit' }).click(); + await expect(page.getByText('Success')).toBeVisible(); + }); +}); +``` + +### Loading States +```typescript +test('should handle loading content', async ({ page }) => { + await page.goto('/'); + + // Wait for content to load using test IDs + await page.waitForSelector('[data-testid="dog-list-grid"]', { timeout: 10000 }); + + // Verify loading is complete + await expect(page.getByTestId('dog-list-loading')).not.toBeVisible(); + await expect(page.getByTestId('dog-card-1')).toBeVisible(); +}); +``` + +### Navigation Flow +```typescript +test('should navigate dog details workflow', async ({ page }) => { + await page.goto('/'); + + // Wait for dogs to load using test ID + await page.waitForSelector('[data-testid="dog-list-grid"]', { timeout: 10000 }); + + // Click first dog using test ID + await page.getByTestId('dog-card-1').click(); + + // Verify navigation + await expect(page.url()).toMatch(/\/dog\/\d+/); + await expect(page).toHaveTitle(/Dog Details/); + await expect(page.getByTestId('dog-details-container')).toBeVisible(); + + // Navigate back using test ID + await page.getByTestId('back-to-dogs-button').click(); + await expect(page).toHaveURL('/'); + await expect(page.getByTestId('homepage-container')).toBeVisible(); +}); +``` + +### Error Handling +```typescript +test('should handle API errors', async ({ page }) => { + // Mock API failure + await page.route('/api/dogs', route => { + route.fulfill({ + status: 500, + body: JSON.stringify({ error: 'Server Error' }) + }); + }); + + await page.goto('/'); + + // Verify error state using test ID + await expect(page.getByTestId('dog-list-error')).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId('error-message')).toContainText('Failed to fetch'); +}); +``` + +## Common Assertions + +```typescript +// Page state +await expect(page).toHaveTitle(/Expected Title/); +await expect(page).toHaveURL('/path'); + +// Element visibility using test IDs +await expect(page.getByTestId('homepage-title')).toBeVisible(); +await expect(page.getByTestId('dog-list-loading')).not.toBeVisible(); + +// Element content using test IDs +await expect(page.getByTestId('dog-name-1')).toContainText('Buddy'); +await expect(page.getByTestId('error-message')).toContainText('Failed to fetch'); + +// Element states using test IDs +await expect(page.getByTestId('submit-button')).toBeEnabled(); +await expect(page.getByTestId('search-input')).toHaveValue('search term'); +``` + +## File Organization + +- `homepage.spec.ts` - Main page tests +- `dog-details.spec.ts` - Individual dog page tests +- `api-integration.spec.ts` - API error scenarios +- `navigation.spec.ts` - Navigation workflows + +## Running Tests + +```bash +npm run test:e2e # Run all tests +npm run test:e2e:ui # Debug with UI +npm run test:e2e:headed # See browser +``` + +## Key Tips + +- **Use test IDs first**: Always prefer `data-testid` attributes for reliable element identification +- Use `page.waitForSelector('[data-testid="element"]')` for dynamic content, not `networkidle` +- Group tests with `test.describe()` and descriptive names +- Set reasonable timeouts (5-10 seconds) +- Test real user scenarios, not implementation details +- Include both happy path and error scenarios in your test suites diff --git a/.github/instructions/svelte.instructions.md b/.github/instructions/svelte.instructions.md new file mode 100644 index 0000000..f4aa9a5 --- /dev/null +++ b/.github/instructions/svelte.instructions.md @@ -0,0 +1,263 @@ +--- +applyTo: "**/*.svelte" +--- + +# Svelte Component Guidelines for Tailspin Shelter + +Essential patterns for writing Svelte 5.23+ components with TypeScript in the dark-themed Tailspin Shelter application. + +## Core Principles + +1. **TypeScript First** - Always use TypeScript for type safety +2. **Dark Theme** - Use slate color palette (`bg-slate-800`, `text-slate-100`, `border-slate-700`) +3. **Responsive** - Mobile-first with Tailwind responsive prefixes +4. **Accessibility** - Semantic HTML and ARIA attributes +5. **Test Identifiers** - Always include `data-testid` attributes for E2E testing + +> 📋 **Reference**: See [`test-identifiers.md`](./test-identifiers.md) for complete list of required test IDs + +## Basic Component Structure + +```svelte + + + +
+

+ {title} +

+ + {#if loading} + +
+ +
+ {:else if error} + +
+

{error}

+
+ {:else} + +
+ +
+ {/if} +
+``` + +## API Data Fetching + +```svelte + + +
+ {#if loading} +
+
+
+
+ {:else if error} +
+

{error}

+
+ {:else if apiData.length === 0} +
+

No data available.

+
+ {:else} +
+ {#each apiData as item (item.id)} +
+

{item.name}

+ +
+ {/each} +
+ {/if} +
+``` + +## Dark Theme Card Pattern + +```svelte + +
+

{title}

+

{description}

+ +
+ + + +
+
+
+

{title}

+
+ View details + + + +
+
+
+
+``` + +## Form Handling + +```svelte + + +
+
+ + + {#if errors.name}

{errors.name}

{/if} +
+ + +
+``` + +## Component Events + +```svelte + + + +``` + +## Accessibility & Testing + +```svelte + +
+

Items

+ +
    + {#each items as item (item.id)} +
  • + +
  • + {/each} +
+
+``` + +## Key Patterns Summary + +- **TypeScript interfaces** for all data structures +- **Dark theme colors**: `bg-slate-800`, `text-slate-100`, `border-slate-700` +- **Hover effects**: `transition-all duration-300`, `group-hover:` classes +- **Loading skeletons**: `animate-pulse` with `bg-slate-700` placeholders +- **Error handling**: Red backgrounds with `bg-red-500/20` and `text-red-400` +- **Responsive grids**: `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3` +- **Accessibility**: Use semantic HTML, ARIA labels, and `data-testid` attributes diff --git a/.github/instructions/test-identifiers.md b/.github/instructions/test-identifiers.md new file mode 100644 index 0000000..b858632 --- /dev/null +++ b/.github/instructions/test-identifiers.md @@ -0,0 +1,143 @@ +# Test Identifiers Reference + +This document provides a comprehensive reference for all `data-testid` attributes used in the Tailspin Shelter application for E2E testing with Playwright. + +## Why Test Identifiers? + +Test identifiers (`data-testid` attributes) provide stable, reliable selectors for E2E tests that won't break when: +- CSS classes change +- Styling is updated +- Component structure is refactored +- Text content is modified + +## Naming Conventions + +- **Format**: Use kebab-case (`dog-card-1`, `homepage-title`) +- **Descriptive**: Include component and purpose in the name +- **Unique**: Append IDs for repeated elements (`dog-card-${id}`) +- **Hierarchical**: Use prefixes for related elements (`dog-details-name`, `dog-details-breed`) + +## Component Test IDs + +### Homepage (`src/pages/index.astro`) +- `homepage-container`: Main page wrapper +- `homepage-title`: Welcome heading +- `homepage-description`: Descriptive text + +### Dog List (`src/components/DogList.svelte`) +- `dog-list-container`: Main container +- `available-dogs-heading`: Section heading +- `dog-list-grid`: Grid of dog cards +- `dog-list-loading`: Loading state container +- `dog-list-error`: Error state container +- `dog-list-empty`: Empty state container +- `dog-card-{id}`: Individual dog cards (with ID) +- `dog-name-{id}`: Dog name within card +- `dog-breed-{id}`: Dog breed within card +- `dog-view-details-{id}`: View details link + +### Dog Details (`src/components/DogDetails.svelte`) +- `dog-details-page`: Page container +- `dog-details-container`: Main details container +- `dog-details-loading`: Loading state +- `dog-details-error`: Error state +- `dog-details-no-data`: No data state +- `dog-details-name`: Dog name heading +- `dog-details-breed`: Breed information +- `dog-details-age`: Age information +- `dog-details-gender`: Gender information +- `dog-details-description`: About text +- `dog-details-about-heading`: About section heading +- `dog-status-available`: Available status badge +- `dog-status-pending`: Pending status badge +- `dog-status-adopted`: Adopted status badge + +### Navigation +- `back-to-dogs-button`: Navigation button (dog details and about pages) +- `navigation-section`: Navigation container + +### About Page (`src/pages/about.astro`) +- `about-page-container`: Page wrapper +- `about-page-title`: Page heading +- `about-page-content`: Main content area +- `about-page-description-1`: First paragraph +- `about-page-description-2`: Second paragraph +- `about-page-note`: Disclaimer section +- `fictional-organization-note`: Fictional org note +- `about-page-navigation`: Navigation section + +### Header (`src/components/Header.astro`) +- `site-header`: Header container +- `menu-toggle-button`: Hamburger menu button +- `navigation-menu`: Dropdown menu +- `nav-home-link`: Home navigation link +- `nav-about-link`: About navigation link +- `site-title`: Site title heading +- `site-title-link`: Site title link + +### Common State Elements +- `{component}-loading`: Loading states +- `{component}-error`: Error states +- `{component}-empty`: Empty states +- `error-message`: Error message text +- `empty-message`: Empty state message text + +## Usage in Tests + +### Preferred Locator Strategy +```typescript +// 1. Test IDs (most reliable) +await page.getByTestId('dog-card-1').click(); +await expect(page.getByTestId('homepage-title')).toBeVisible(); + +// 2. Semantic locators (when test IDs aren't available) +await page.getByRole('heading', { name: 'Welcome' }).click(); + +// 3. Text content (for validation) +await expect(page.getByTestId('dog-name-1')).toContainText('Buddy'); +``` + +### Common Patterns +```typescript +// Wait for content to load +await page.waitForSelector('[data-testid="dog-list-grid"]', { timeout: 10000 }); + +// Navigate to dog details +await page.getByTestId('dog-card-1').click(); +await expect(page.getByTestId('dog-details-container')).toBeVisible(); + +// Handle error states +await expect(page.getByTestId('dog-list-error')).toBeVisible(); +await expect(page.getByTestId('error-message')).toContainText('Failed to fetch'); +``` + +## Adding New Test IDs + +When creating new components or pages: + +1. **Container**: Always add a main container ID +2. **States**: Include loading, error, and empty states +3. **Interactive Elements**: Buttons, links, form inputs +4. **Content**: Important headings and text +5. **Lists**: Individual items with unique identifiers + +Example: +```svelte +
+ {#if loading} +
Loading...
+ {:else if error} +
+

{error}

+
+ {:else} +
+ {#each items as item} +
+

{item.title}

+
+ {/each} +
+ {/if} +
+``` diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..9ffbda4 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,45 @@ +name: End-to-End Tests + +on: + push: + branches: [ main, update-workshop ] + pull_request: + branches: [ main ] + +jobs: + e2e-tests: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: client/package-lock.json + + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install dependencies + working-directory: ./client + run: npm ci + + - name: Set up Python environment and dependencies + run: ./scripts/setup-environment.sh + + - name: Install Playwright Browsers + working-directory: ./client + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: ./scripts/run-e2e-tests.sh + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: client/playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 7f10982..60498f1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,8 @@ instance/ .webassets-cache flask_session/ .coverage -htmlcov/ \ No newline at end of file +htmlcov/ + +# playwright +client/test-results/ +client/playwright-report/ \ No newline at end of file diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..aa07b42 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,10 @@ +{ + "servers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index c8a0d1b..d011cf6 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,24 @@ This repository contains the project for two guided workshops to explore various > **[Get started learning about development with GitHub!](./content/README.md)** +## Development + +### Quick Start + +To run the application: + +```bash +./scripts/start-app.sh +``` + +This will start both the Flask backend (port 5100) and Astro frontend (port 4321). + +### Project Structure + +- `client/` - Astro frontend with Svelte components +- `server/` - Flask backend with SQLAlchemy models +- `scripts/` - Development and testing scripts + ## License This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE.txt) for the full terms. diff --git a/client/README.md b/client/README.md index ff19a3e..91ee67d 100644 --- a/client/README.md +++ b/client/README.md @@ -43,6 +43,20 @@ All commands are run from the root of the project, from a terminal: | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | | `npm run astro -- --help` | Get help using the Astro CLI | +## 🧪 Testing + +For end-to-end testing, we use Playwright. All test commands should be run from the **project root**: + +| Command | Action | +| :----------------------------- | :-------------------------------------------------- | +| `./scripts/run-e2e-tests.sh` | Runs all Playwright e2e tests (recommended) | +| `npm run test:e2e` | Runs all e2e tests (from client directory) | +| `npm run test:e2e:ui` | Opens Playwright UI for interactive testing | +| `npm run test:e2e:debug` | Runs tests in debug mode | +| `npm run test:e2e:headed` | Runs tests with browser UI visible | + +**Note:** The script `./scripts/run-e2e-tests.sh` is the recommended way to run tests as it handles environment setup automatically. + ## 👀 Want to learn more? Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/client/e2e-tests/README.md b/client/e2e-tests/README.md new file mode 100644 index 0000000..b7d198c --- /dev/null +++ b/client/e2e-tests/README.md @@ -0,0 +1,70 @@ +# End-to-End Tests for Tailspin Shelter + +This directory contains Playwright end-to-end tests for the Tailspin Shelter website. + +## Test Files + +- `homepage.spec.ts` - Tests for the main homepage functionality +- `about.spec.ts` - Tests for the about page +- `dog-details.spec.ts` - Tests for individual dog detail pages +- `api-integration.spec.ts` - Tests for API integration and error handling + +## Running Tests + +### Prerequisites + +Make sure you have installed dependencies: +```bash +npm install +``` + +### Running Tests + +```bash +# Run all tests +npm run test:e2e + +# Run tests with UI mode (for debugging) +npm run test:e2e:ui + +# Run tests in headed mode (see browser) +npm run test:e2e:headed + +# Run only WebKit tests +npm run test:e2e:webkit + +# Debug tests +npm run test:e2e:debug +``` + +## Test Coverage + +The tests cover the following core functionality: + +### Homepage Tests +- Page loads with correct title and content +- Dog list displays properly +- Loading states work correctly +- Error handling for API failures + +### About Page Tests +- About page content displays correctly +- Navigation back to homepage works + +### Dog Details Tests +- Navigation from homepage to dog details +- Navigation back from dog details to homepage +- Handling of invalid dog IDs + +### API Integration Tests +- Successful API responses +- Empty dog list handling +- Network error handling + +## Configuration + +Tests are configured in `../playwright.config.ts` and automatically start the application servers using the existing `scripts/start-app.sh` script before running tests. + +The tests run against: +- Client (Astro): http://localhost:4321 +- Server (Flask): http://localhost:5100 \ No newline at end of file diff --git a/client/e2e-tests/about.spec.ts b/client/e2e-tests/about.spec.ts new file mode 100644 index 0000000..d37aeee --- /dev/null +++ b/client/e2e-tests/about.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; + +test.describe('About Page', () => { + test('should load about page and display content', async ({ page }) => { + await page.goto('/about'); + + // Check that the page title is correct + await expect(page).toHaveTitle(/About - Tailspin Shelter/); + + // Check that the main elements are visible using test IDs + await expect(page.getByTestId('about-page-container')).toBeVisible(); + await expect(page.getByTestId('about-page-title')).toBeVisible(); + await expect(page.getByTestId('about-page-title')).toContainText('About Tailspin Shelter'); + + // Check that content is visible + await expect(page.getByTestId('about-page-description-1')).toContainText('Nestled in the heart of Seattle'); + await expect(page.getByTestId('about-page-description-2')).toContainText('The name "Tailspin" reflects'); + + // Check the fictional organization note + await expect(page.getByTestId('fictional-organization-note')).toContainText('Tailspin Shelter is a fictional organization'); + }); + + test('should navigate back to homepage from about page', async ({ page }) => { + await page.goto('/about'); + + // Wait for page to load + await expect(page.getByTestId('about-page-container')).toBeVisible(); + + // Click the "Back to Dogs" button using test ID + await page.getByTestId('back-to-dogs-button').click(); + + // Should be redirected to homepage + await expect(page).toHaveURL('/'); + await expect(page.getByTestId('homepage-container')).toBeVisible(); + await expect(page.getByTestId('homepage-title')).toContainText('Welcome to Tailspin Shelter'); + }); +}); \ No newline at end of file diff --git a/client/e2e-tests/api-integration.spec.ts b/client/e2e-tests/api-integration.spec.ts new file mode 100644 index 0000000..16d58a9 --- /dev/null +++ b/client/e2e-tests/api-integration.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; + +test.describe('API Integration', () => { + test('should fetch dogs from API', async ({ page }) => { + // Mock successful API response + await page.route('/api/dogs', route => { + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { id: 1, name: 'Buddy', breed: 'Golden Retriever' }, + { id: 2, name: 'Luna', breed: 'Husky' }, + { id: 3, name: 'Max', breed: 'Labrador' } + ]) + }); + }); + + await page.goto('/'); + + // Wait for dog list to load + await expect(page.getByTestId('dog-list-grid')).toBeVisible({ timeout: 10000 }); + + // Check that mocked dogs are displayed using test IDs + await expect(page.getByTestId('dog-name-1')).toContainText('Buddy'); + await expect(page.getByTestId('dog-breed-1')).toContainText('Golden Retriever'); + await expect(page.getByTestId('dog-name-2')).toContainText('Luna'); + await expect(page.getByTestId('dog-breed-2')).toContainText('Husky'); + await expect(page.getByTestId('dog-name-3')).toContainText('Max'); + await expect(page.getByTestId('dog-breed-3')).toContainText('Labrador'); + }); + + test('should handle empty dog list', async ({ page }) => { + // Mock empty API response + await page.route('/api/dogs', route => { + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([]) + }); + }); + + await page.goto('/'); + + // Check that empty state message is displayed using test IDs + await expect(page.getByTestId('dog-list-empty')).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId('empty-message')).toContainText('No dogs available at the moment'); + }); + + test('should handle network errors', async ({ page }) => { + // Mock network error + await page.route('/api/dogs', route => { + route.abort('failed'); + }); + + await page.goto('/'); + + // Check that error message is displayed using test IDs + await expect(page.getByTestId('dog-list-error')).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId('error-message')).toContainText('Error:'); + }); +}); \ No newline at end of file diff --git a/client/e2e-tests/dog-details.spec.ts b/client/e2e-tests/dog-details.spec.ts new file mode 100644 index 0000000..14f1659 --- /dev/null +++ b/client/e2e-tests/dog-details.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Dog Details', () => { + test('should navigate to dog details from homepage', async ({ page }) => { + await page.goto('/'); + + // Wait for dogs to load using test ID + await page.waitForSelector('[data-testid="dog-list-grid"]', { timeout: 10000 }); + + // Get the first dog card using test ID + const firstDogCard = page.getByTestId('dog-card-1'); + await expect(firstDogCard).toBeVisible(); + + // Get the dog name for verification + const dogName = await firstDogCard.getByTestId('dog-name-1').textContent(); + + // Click on the first dog + await firstDogCard.click(); + + // Should be on a dog details page + await expect(page.url()).toMatch(/\/dog\/1/); + + // Check that the page title is correct + await expect(page).toHaveTitle(/Dog Details - Tailspin Shelter/); + + // Check for dog details container and back button using test IDs + await expect(page.getByTestId('dog-details-container')).toBeVisible(); + await expect(page.getByTestId('back-to-dogs-button')).toBeVisible(); + + // Verify the dog name matches + if (dogName) { + await expect(page.getByTestId('dog-details-name')).toContainText(dogName); + } + }); + + test('should navigate back to homepage from dog details', async ({ page }) => { + // Go directly to a dog details page (assuming dog with ID 1 exists) + await page.goto('/dog/1'); + + // Wait for dog details to load + await expect(page.getByTestId('dog-details-page')).toBeVisible(); + + // Click the back button using test ID + await page.getByTestId('back-to-dogs-button').click(); + + // Should be redirected to homepage + await expect(page).toHaveURL('/'); + await expect(page.getByTestId('homepage-container')).toBeVisible(); + await expect(page.getByTestId('homepage-title')).toContainText('Welcome to Tailspin Shelter'); + }); + + test('should handle invalid dog ID gracefully', async ({ page }) => { + // Go to a dog page with an invalid ID + await page.goto('/dog/99999'); + + // The page should still load (even if no dog is found) + await expect(page).toHaveTitle(/Dog Details - Tailspin Shelter/); + await expect(page.getByTestId('dog-details-page')).toBeVisible(); + + // Back button should still be available + await expect(page.getByTestId('back-to-dogs-button')).toBeVisible(); + + // Should show either error state or no data message + try { + await expect(page.getByTestId('dog-details-error')).toBeVisible({ timeout: 5000 }); + } catch { + await expect(page.getByTestId('dog-details-no-data')).toBeVisible(); + } + }); +}); \ No newline at end of file diff --git a/client/e2e-tests/homepage.spec.ts b/client/e2e-tests/homepage.spec.ts new file mode 100644 index 0000000..414157b --- /dev/null +++ b/client/e2e-tests/homepage.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Tailspin Shelter Homepage', () => { + test('should load homepage and display title', async ({ page }) => { + await page.goto('/'); + + // Check that the page title is correct + await expect(page).toHaveTitle(/Tailspin Shelter - Find Your Forever Friend/); + + // Check that the main elements are visible using test IDs + await expect(page.getByTestId('homepage-title')).toBeVisible(); + await expect(page.getByTestId('homepage-description')).toBeVisible(); + + // Verify content + await expect(page.getByTestId('homepage-title')).toContainText('Welcome to Tailspin Shelter'); + await expect(page.getByTestId('homepage-description')).toContainText('Find your perfect companion'); + }); + + test('should display dog list section', async ({ page }) => { + await page.goto('/'); + + // Check that the "Available Dogs" heading is visible + await expect(page.getByTestId('available-dogs-heading')).toBeVisible(); + await expect(page.getByTestId('available-dogs-heading')).toContainText('Available Dogs'); + + // Wait for dogs to load (either loading state, error, or actual dogs) + await page.waitForSelector('[data-testid="dog-list-container"]', { timeout: 10000 }); + }); + + test('should show loading state initially', async ({ page }) => { + await page.goto('/'); + + // Check that loading animation is shown initially + const loadingElements = page.getByTestId('dog-list-loading'); + + // Either loading should be visible initially, or dogs should load quickly + try { + await expect(loadingElements).toBeVisible({ timeout: 2000 }); + } catch { + // If loading finishes too quickly, that's fine - check for dog content instead + await expect(page.getByTestId('dog-list-grid')).toBeVisible(); + } + }); + + test('should handle API errors gracefully', async ({ page }) => { + // Intercept the API call and make it fail + await page.route('/api/dogs', route => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }) + }); + }); + + await page.goto('/'); + + // Check that error message is displayed using test IDs + await expect(page.getByTestId('dog-list-error')).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId('error-message')).toContainText('Failed to fetch data'); + }); +}); \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index fbfbe84..e9e2a78 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,6 +16,7 @@ "typescript": "^5.8.2" }, "devDependencies": { + "@playwright/test": "^1.49.1", "@types/node": "^22.13.11", "autoprefixer": "^10.4.21", "postcss": "^8.5.3", @@ -36,25 +37,25 @@ } }, "node_modules/@astrojs/compiler": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.11.0.tgz", - "integrity": "sha512-zZOO7i+JhojO8qmlyR/URui6LyfHJY6m+L9nwyX5GiKD78YoRaZ5tzz6X0fkl+5bD3uwlDHayf6Oe8Fu36RKNg==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.12.2.tgz", + "integrity": "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw==", "license": "MIT" }, "node_modules/@astrojs/internal-helpers": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.6.1.tgz", - "integrity": "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.2.tgz", + "integrity": "sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g==", "license": "MIT" }, "node_modules/@astrojs/markdown-remark": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.2.1.tgz", - "integrity": "sha512-qtQXfZXeG84XSH9bMgG2e/kZfA4J7U19PKjhmFDNsKX47nautSHC0DitvxaWgQFSED66k6hWKDHLq3VKHCy/rg==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.6.tgz", + "integrity": "sha512-bwylYktCTsLMVoCOEHbn2GSUA3c5KT/qilekBKA3CBng0bo1TYjNZPr761vxumRk9kJGqTOtU+fgCAp5Vwokug==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.6.1", - "@astrojs/prism": "3.2.0", + "@astrojs/internal-helpers": "0.7.2", + "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", @@ -65,10 +66,10 @@ "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.1", + "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", - "shiki": "^1.29.2", - "smol-toml": "^1.3.1", + "shiki": "^3.2.1", + "smol-toml": "^1.3.4", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", @@ -77,29 +78,29 @@ } }, "node_modules/@astrojs/node": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.1.3.tgz", - "integrity": "sha512-YcVxEmeZU8khNdrPYNPN3j//4tYPM+Pw6CthAJ6VE/bw65qEX7ErMRApalY2tibc3YhCeHMmsO9rXGhyW0NNyA==", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.4.3.tgz", + "integrity": "sha512-P9BQHY8wQU1y9obknXzxV5SS3EpdaRnuDuHKr3RFC7t+2AzcMXeVmMJprQGijnQ8VdijJ8aS7+12tx325TURMQ==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.6.1", - "send": "^1.1.0", + "@astrojs/internal-helpers": "0.7.2", + "send": "^1.2.0", "server-destroy": "^1.0.1" }, "peerDependencies": { - "astro": "^5.3.0" + "astro": "^5.7.0" } }, "node_modules/@astrojs/prism": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.2.0.tgz", - "integrity": "sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz", + "integrity": "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==", "license": "MIT", "dependencies": { - "prismjs": "^1.29.0" + "prismjs": "^1.30.0" }, "engines": { - "node": "^18.17.1 || ^20.3.0 || >=22.0.0" + "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, "node_modules/@astrojs/svelte": { @@ -122,13 +123,13 @@ } }, "node_modules/@astrojs/telemetry": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.2.0.tgz", - "integrity": "sha512-wxhSKRfKugLwLlr4OFfcqovk+LIFtKwLyGPqMsv+9/ibqqnW3Gv7tBhtKEb0gAyUAC4G9BTVQeQahqnQAhd6IQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", + "integrity": "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==", "license": "MIT", "dependencies": { - "ci-info": "^4.1.0", - "debug": "^4.3.7", + "ci-info": "^4.2.0", + "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", @@ -136,7 +137,7 @@ "which-pm-runs": "^1.1.0" }, "engines": { - "node": "^18.17.1 || ^20.3.0 || >=22.0.0" + "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, "node_modules/@babel/helper-string-parser": { @@ -185,6 +186,17 @@ "node": ">=6.9.0" } }, + "node_modules/@capsizecss/unpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-2.4.0.tgz", + "integrity": "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==", + "license": "MIT", + "dependencies": { + "blob-to-buffer": "^1.2.8", + "cross-fetch": "^3.0.4", + "fontkit": "^2.0.2" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -1010,6 +1022,22 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, + "node_modules/@playwright/test": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", + "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", @@ -1286,65 +1314,63 @@ ] }, "node_modules/@shikijs/core": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", - "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.12.0.tgz", + "integrity": "sha512-rPfCBd6gHIKBPpf2hKKWn2ISPSrmRKAFi+bYDjvZHpzs3zlksWvEwaF3Z4jnvW+xHxSRef7qDooIJkY0RpA9EA==", "license": "MIT", "dependencies": { - "@shikijs/engine-javascript": "1.29.2", - "@shikijs/engine-oniguruma": "1.29.2", - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", + "@shikijs/types": "3.12.0", + "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.4" + "hast-util-to-html": "^9.0.5" } }, "node_modules/@shikijs/engine-javascript": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", - "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.12.0.tgz", + "integrity": "sha512-Ni3nm4lnKxyKaDoXQQJYEayX052BL7D0ikU5laHp+ynxPpIF1WIwyhzrMU6WDN7AoAfggVR4Xqx3WN+JTS+BvA==", "license": "MIT", "dependencies": { - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", - "oniguruma-to-es": "^2.2.0" + "@shikijs/types": "3.12.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.3" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", - "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.12.0.tgz", + "integrity": "sha512-IfDl3oXPbJ/Jr2K8mLeQVpnF+FxjAc7ZPDkgr38uEw/Bg3u638neSrpwqOTnTHXt1aU0Fk1/J+/RBdst1kVqLg==", "license": "MIT", "dependencies": { - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1" + "@shikijs/types": "3.12.0", + "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", - "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.12.0.tgz", + "integrity": "sha512-HIca0daEySJ8zuy9bdrtcBPhcYBo8wR1dyHk1vKrOuwDsITtZuQeGhEkcEfWc6IDyTcom7LRFCH6P7ljGSCEiQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "1.29.2" + "@shikijs/types": "3.12.0" } }, "node_modules/@shikijs/themes": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", - "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.12.0.tgz", + "integrity": "sha512-/lxvQxSI5s4qZLV/AuFaA4Wt61t/0Oka/P9Lmpr1UV+HydNCczO3DMHOC/CsXCCpbv4Zq8sMD0cDa7mvaVoj0Q==", "license": "MIT", "dependencies": { - "@shikijs/types": "1.29.2" + "@shikijs/types": "3.12.0" } }, "node_modules/@shikijs/types": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", - "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.12.0.tgz", + "integrity": "sha512-jsFzm8hCeTINC3OCmTZdhR9DOl/foJWplH2Px0bTi4m8z59fnsueLsweX82oGcjRQ7mfQAluQYKGoH2VzsWY4A==", "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.1", + "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, @@ -1401,6 +1427,15 @@ "vite": "^6.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tailwindcss/node": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.13.tgz", @@ -1637,12 +1672,6 @@ "integrity": "sha512-gbvFrB0fOsTv/OugXWi2PtflJ4S6/ctu6Mmn3bCftmLY/6xRsQVEJPgIIpABwpZ52DpONkCA3bEj5b54MHxF2Q==", "license": "MIT" }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT" - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1658,6 +1687,15 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/fontkit": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/fontkit/-/fontkit-2.0.8.tgz", + "integrity": "sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -1695,7 +1733,6 @@ "version": "22.13.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.11.tgz", "integrity": "sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -1850,26 +1887,26 @@ } }, "node_modules/astro": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/astro/-/astro-5.4.3.tgz", - "integrity": "sha512-GKkOJQCHLx6CrPoghGhj7824WDSvIuuc+HTVjfjMPdB9axp238iJLByREJNDaSdzMeR/lC13xvBiUnKvcYyEIA==", + "version": "5.13.4", + "resolved": "https://registry.npmjs.org/astro/-/astro-5.13.4.tgz", + "integrity": "sha512-Mgq5GYy3EHtastGXqdnh1UPuN++8NmJSluAspA5hu33O7YRs/em/L03cUfRXtc60l5yx5BfYJsjF2MFMlcWlzw==", "license": "MIT", "dependencies": { - "@astrojs/compiler": "^2.10.4", - "@astrojs/internal-helpers": "0.6.1", - "@astrojs/markdown-remark": "6.2.1", - "@astrojs/telemetry": "3.2.0", + "@astrojs/compiler": "^2.12.2", + "@astrojs/internal-helpers": "0.7.2", + "@astrojs/markdown-remark": "6.3.6", + "@astrojs/telemetry": "3.3.0", + "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", - "@types/cookie": "^0.6.0", - "acorn": "^8.14.0", + "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", - "ci-info": "^4.1.0", + "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", - "cookie": "^0.7.2", + "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", @@ -1881,9 +1918,11 @@ "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", + "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", + "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", @@ -1892,33 +1931,35 @@ "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", - "package-manager-detector": "^1.0.0", + "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", - "shiki": "^1.29.2", + "shiki": "^3.2.1", + "smol-toml": "^1.3.4", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", - "ultrahtml": "^1.5.3", + "ultrahtml": "^1.6.0", + "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", - "vite": "^6.2.0", + "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", - "zod": "^3.24.2", - "zod-to-json-schema": "^3.24.3", + "zod": "^3.24.4", + "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "bin": { "astro": "astro.js" }, "engines": { - "node": "^18.17.1 || ^20.3.0 || >=22.0.0", + "node": "18.20.8 || ^20.3.0 || >=22.0.0", "npm": ">=9.6.5", "pnpm": ">=7.1.0" }, @@ -1993,6 +2034,46 @@ "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/blob-to-buffer": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz", + "integrity": "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/boxen": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", @@ -2015,6 +2096,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -2149,9 +2239,9 @@ } }, "node_modules/ci-info": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", - "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", "funding": [ { "type": "github", @@ -2175,6 +2265,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2246,12 +2345,12 @@ "license": "ISC" }, "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/cookie-es": { @@ -2260,6 +2359,15 @@ "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", "license": "MIT" }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/crossws": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.4.tgz", @@ -2269,6 +2377,19 @@ "uncrypto": "^0.1.3" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2299,9 +2420,9 @@ } }, "node_modules/decode-named-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", - "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -2356,16 +2477,6 @@ "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", "license": "MIT" }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -2388,9 +2499,9 @@ } }, "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz", + "integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==", "license": "MIT" }, "node_modules/devlop": { @@ -2406,6 +2517,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -2449,12 +2566,6 @@ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, - "node_modules/emoji-regex-xs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", - "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2608,6 +2719,12 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fdir": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", @@ -2631,6 +2748,33 @@ "node": ">=8" } }, + "node_modules/fontace": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.3.0.tgz", + "integrity": "sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg==", + "license": "MIT", + "dependencies": { + "@types/fontkit": "^2.0.8", + "fontkit": "^2.0.4" + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2646,12 +2790,12 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fsevents": { @@ -2934,6 +3078,15 @@ "node": ">= 0.8" } }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/import-meta-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", @@ -3588,6 +3741,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -4152,21 +4311,21 @@ "license": "MIT" }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -4237,6 +4396,26 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", @@ -4286,6 +4465,12 @@ "ufo": "^1.5.4" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4298,15 +4483,21 @@ "node": ">= 0.8" } }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, "node_modules/oniguruma-to-es": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", - "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz", + "integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==", "license": "MIT", "dependencies": { - "emoji-regex-xs": "^1.0.0", - "regex": "^5.1.1", - "regex-recursion": "^5.1.1" + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" } }, "node_modules/p-limit": { @@ -4353,9 +4544,15 @@ } }, "node_modules/package-manager-detector": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.0.0.tgz", - "integrity": "sha512-7elnH+9zMsRo7aS72w6MeRugTpdRvInmEB4Kmm9BVvPw/SLG8gXUGQ+4wF0Mys0RSWPz0B9nuBbDe8vFeA2sfg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "license": "MIT" }, "node_modules/parse-latin": { @@ -4416,6 +4613,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -4521,21 +4765,20 @@ } }, "node_modules/regex": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", - "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", + "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" } }, "node_modules/regex-recursion": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", - "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", "license": "MIT", "dependencies": { - "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, @@ -4641,9 +4884,9 @@ } }, "node_modules/remark-rehype": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", - "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -4687,6 +4930,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/retext": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", @@ -4799,19 +5048,18 @@ } }, "node_modules/send": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", - "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", - "fresh": "^0.5.2", + "fresh": "^2.0.0", "http-errors": "^2.0.0", - "mime-types": "^2.1.35", + "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", @@ -4874,18 +5122,18 @@ } }, "node_modules/shiki": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", - "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", - "license": "MIT", - "dependencies": { - "@shikijs/core": "1.29.2", - "@shikijs/engine-javascript": "1.29.2", - "@shikijs/engine-oniguruma": "1.29.2", - "@shikijs/langs": "1.29.2", - "@shikijs/themes": "1.29.2", - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.12.0.tgz", + "integrity": "sha512-E+ke51tciraTHpaXYXfqnPZFSViKHhSQ3fiugThlfs/om/EonlQ0hSldcqgzOWWqX6PcjkKKzFgrjIaiPAXoaA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.12.0", + "@shikijs/engine-javascript": "3.12.0", + "@shikijs/engine-oniguruma": "3.12.0", + "@shikijs/langs": "3.12.0", + "@shikijs/themes": "3.12.0", + "@shikijs/types": "3.12.0", + "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, @@ -4906,9 +5154,9 @@ "license": "MIT" }, "node_modules/smol-toml": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.1.tgz", - "integrity": "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz", + "integrity": "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -4937,9 +5185,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -5046,6 +5294,12 @@ "node": ">=6" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -5077,6 +5331,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -5155,9 +5415,9 @@ "license": "MIT" }, "node_modules/ultrahtml": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz", - "integrity": "sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", "license": "MIT" }, "node_modules/uncrypto": { @@ -5170,9 +5430,28 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "devOptional": true, "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -5192,6 +5471,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unifont": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.5.2.tgz", + "integrity": "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0", + "ofetch": "^1.4.1", + "ohash": "^2.0.0" + } + }, "node_modules/unist-util-find-after": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", @@ -5582,6 +5872,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which-pm-runs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", @@ -5684,18 +5990,18 @@ "license": "MIT" }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz", - "integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "license": "ISC", "peerDependencies": { "zod": "^3.24.1" diff --git a/client/package.json b/client/package.json index 1e13e95..165cae7 100644 --- a/client/package.json +++ b/client/package.json @@ -6,7 +6,12 @@ "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug", + "test:e2e:headed": "playwright test --headed", + "test:e2e:chromium": "playwright test --project=chromium" }, "dependencies": { "@astrojs/node": "^9.1.3", @@ -17,6 +22,7 @@ "typescript": "^5.8.2" }, "devDependencies": { + "@playwright/test": "^1.49.1", "@types/node": "^22.13.11", "autoprefixer": "^10.4.21", "postcss": "^8.5.3", diff --git a/client/playwright.config.ts b/client/playwright.config.ts new file mode 100644 index 0000000..9c9b0d5 --- /dev/null +++ b/client/playwright.config.ts @@ -0,0 +1,42 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './e2e-tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'line', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:4321', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'cd .. && ./scripts/start-app.sh', + url: 'http://localhost:4321', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); \ No newline at end of file diff --git a/client/run-tests.sh b/client/run-tests.sh new file mode 100755 index 0000000..4d38e9d --- /dev/null +++ b/client/run-tests.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Simple test script that starts the app and runs Playwright tests +set -e + +echo "Starting application servers in background..." + +# Store current directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/.." + +# Start the application using the existing script +./scripts/start-app.sh & +APP_PID=$! + +# Function to cleanup +cleanup() { + echo "Cleaning up..." + kill $APP_PID 2>/dev/null || true + pkill -f "python.*app.py" 2>/dev/null || true + pkill -f "npm.*dev" 2>/dev/null || true + wait $APP_PID 2>/dev/null || true +} + +# Trap signals to ensure cleanup +trap cleanup EXIT INT TERM + +echo "Waiting for servers to start..." +sleep 10 + +# Check if servers are running +for i in {1..30}; do + if curl -s http://localhost:4321 > /dev/null && curl -s http://localhost:5100/api/dogs > /dev/null; then + echo "Servers are ready!" + break + fi + if [ $i -eq 30 ]; then + echo "Servers failed to start" + exit 1 + fi + echo "Waiting for servers... ($i/30)" + sleep 2 +done + +echo "Running Playwright tests..." +cd client +npx playwright test "$@" + +echo "Tests completed!" \ No newline at end of file diff --git a/client/src/components/DogDetails.svelte b/client/src/components/DogDetails.svelte index d695277..0c579b0 100644 --- a/client/src/components/DogDetails.svelte +++ b/client/src/components/DogDetails.svelte @@ -49,48 +49,48 @@ {#if loading} -
+
{:else if error} -
- {error} +
+

{error}

{:else if dogData} -
+
-

{dogData.name}

+

{dogData.name}

{#if dogData.status === 'AVAILABLE'} - Available + Available {:else if dogData.status === 'PENDING'} - Pending Adoption + Pending Adoption {:else} - Adopted + Adopted {/if}
-
+
-

Breed: {dogData.breed}

+

Breed: {dogData.breed}

-

Age: {dogData.age} {dogData.age === 1 ? 'year' : 'years'}

+

Age: {dogData.age} {dogData.age === 1 ? 'year' : 'years'}

-

Gender: {dogData.gender}

+

Gender: {dogData.gender}

-

About {dogData.name}

-

{dogData.description}

+

About {dogData.name}

+

{dogData.description}

{:else} -
-

No dog information available

+
+

No dog information available

{/if} \ No newline at end of file diff --git a/client/src/components/DogList.svelte b/client/src/components/DogList.svelte index b28bc36..3321ba1 100644 --- a/client/src/components/DogList.svelte +++ b/client/src/components/DogList.svelte @@ -32,14 +32,14 @@ }); -
-

Available Dogs

+
+

Available Dogs

{#if loading} -
+
{#each Array(6) as _, i} -
+
@@ -52,28 +52,31 @@
{:else if error} -
-

{error}

+
+

{error}

{:else if dogs.length === 0} -
-

No dogs available at the moment.

+
+

No dogs available at the moment.

{:else} -
+
{#each dogs as dog (dog.id)}
-

{dog.name}

-

{dog.breed}

-
+

{dog.name}

+

{dog.breed}

+
View details diff --git a/client/src/components/Header.astro b/client/src/components/Header.astro index 71766c2..7ace218 100644 --- a/client/src/components/Header.astro +++ b/client/src/components/Header.astro @@ -2,23 +2,23 @@ // Import any necessary dependencies --- -
+
diff --git a/client/src/middleware.ts b/client/src/middleware.ts index db2a2dd..eac297b 100644 --- a/client/src/middleware.ts +++ b/client/src/middleware.ts @@ -5,16 +5,12 @@ const API_SERVER_URL = process.env.API_SERVER_URL || 'http://localhost:5100'; // Middleware to handle API requests export const onRequest = defineMiddleware(async (context, next) => { - console.log('Request URL:', context.request.url); // Guard clause: if not an API request, pass through to regular Astro handling if (!context.request.url.includes('/api/')) { return await next(); } - // API request handling - console.log('Forwarding request to server:', API_SERVER_URL); - const url = new URL(context.request.url); const apiPath = url.pathname + url.search; diff --git a/client/src/pages/about.astro b/client/src/pages/about.astro index f31bec8..3cd36db 100644 --- a/client/src/pages/about.astro +++ b/client/src/pages/about.astro @@ -5,28 +5,28 @@ import "../styles/global.css"; --- -
+
-

About Tailspin Shelter

+

About Tailspin Shelter

-
-

+

+

Nestled in the heart of Seattle, Tailspin Shelter is a warm, welcoming haven for dogs of all breeds and backgrounds. Founded in 2015 by a small group of dog lovers and rescue advocates, our mission is to give every dog a second chance at a happy, healthy life. Whether it's finding forever homes, providing medical care, or offering behavioral training, our dedicated team works tirelessly to ensure each dog's journey has a joyful destination. Inspired by Seattle's vibrant and compassionate community, we've grown from a small foster network into a full-service shelter and adoption center.

-

+

The name "Tailspin" reflects both the whirlwind of emotions dogs often experience when they first arrive — and the joyful, spinning tails we see when they find their perfect match. For us, a "tailspin" is a reminder of the transformations we witness daily: from uncertainty and fear to trust, love, and boundless energy. It's this journey that fuels our work and reminds us why we do what we do. Every wagging tail is a testament to resilience and hope.

-
-

+

+

Note: Tailspin Shelter is a fictional organization, created for this workshop.

-
- +
+ diff --git a/client/src/pages/dog/[id].astro b/client/src/pages/dog/[id].astro index 484dc97..8f9ff63 100644 --- a/client/src/pages/dog/[id].astro +++ b/client/src/pages/dog/[id].astro @@ -10,11 +10,11 @@ const props = { dogId }; --- -
+
-
- +
+ diff --git a/client/src/pages/index.astro b/client/src/pages/index.astro index 0c36e02..a256ea7 100644 --- a/client/src/pages/index.astro +++ b/client/src/pages/index.astro @@ -5,10 +5,10 @@ import "../styles/global.css"; --- -
-
-

Welcome to Tailspin Shelter

-

Find your perfect companion from our wonderful selection of dogs looking for their forever homes.

+
+
+

Welcome to Tailspin Shelter

+

Find your perfect companion from our wonderful selection of dogs looking for their forever homes.

diff --git a/content/full-day/0-setup.md b/content/0-setup.md similarity index 67% rename from content/full-day/0-setup.md rename to content/0-setup.md index 3bb7aa3..af74030 100644 --- a/content/full-day/0-setup.md +++ b/content/0-setup.md @@ -1,6 +1,6 @@ # Workshop setup -| [← Modern DevOps with GitHub][walkthrough-previous] | [Next: Enable Code Scanning →][walkthrough-next] | +| [← Modern DevOps with GitHub][previous] | [Next: Enable Code Scanning →][next] | |:-----------------------------------|------------------------------------------:| To complete this workshop you will need to create a repository with a copy of the contents of this repository. While this can be done by [forking a repository][fork-repo], the goal of a fork is to eventually merge code back into the original (or upstream) source. In our case we want a separate copy as we don't intend to merge our changes. This is accomplished through the use of a [template repository][template-repo]. Template repositories are a great way to provide starters for your organization, ensuring consistency across projects. @@ -12,23 +12,24 @@ Let's create the repository you'll use for your workshop. 1. Navigate to [the repository root][repo-root] 2. Select **Use this template** > **Create a new repository** - ![Screenshot of Use this template dropdown](../1-hour/images/0-setup-template.png) + ![Screenshot of Use this template dropdown](./images/0-setup-template.png) 3. Under **Owner**, select the name of your GitHub handle, or the owner specified by your workshop leader. 4. Under **Repository**, set the name to **pets-workshop**, or the name specified by your workshop leader. 5. Ensure **Public** is selected for the visibility, or the value indicated by your workshop leader. 6. Select **Create repository from template**. - ![Screenshot of configured template creation dialog](../1-hour/images/0-setup-configure.png) + ![Screenshot of configured template creation dialog](./images/0-setup-configure.png) In a few moments a new repository will be created from the template for this workshop! ## Summary and next steps -You've now created the repository you'll use for this workshop! Next let's [enable Code Scanning][walkthrough-next] to secure the code we write. +You've now created the repository you'll use for this workshop! Next let's [enable Code Scanning][next] to secure the code we write. -| [← Modern DevOps with GitHub][walkthrough-previous] | [Next: Enable Code Scanning →][walkthrough-next] | +| [← Modern DevOps with GitHub][previous] | [Next: Enable Code Scanning →][next] | |:-----------------------------------|------------------------------------------:| -[fork-repo]: https://docs.github.com/en/get-started/quickstart/fork-a-repo -[template-repo]: https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository +[next]: ./1-code-scanning.md +[previous]: ./README.md + +[fork-repo]: https://docs.github.com/get-started/quickstart/fork-a-repo [repo-root]: / -[walkthrough-previous]: README.md -[walkthrough-next]: 1-code-scanning.md +[template-repo]: https://docs.github.com/repositories/creating-and-managing-repositories/creating-a-template-repository diff --git a/content/full-day/1-code-scanning.md b/content/1-code-scanning.md similarity index 84% rename from content/full-day/1-code-scanning.md rename to content/1-code-scanning.md index cf074c0..c8f7ebd 100644 --- a/content/full-day/1-code-scanning.md +++ b/content/1-code-scanning.md @@ -1,6 +1,6 @@ # Securing the development pipeline -| [← Workshop setup][walkthrough-previous] | [Next: Project management with GitHub Issues →][walkthrough-next] | +| [← Workshop setup][previous] | [Next: Project management with GitHub Issues →][next] | |:-----------------------------------|------------------------------------------:| Ensuring code security is imperative in today's environment. When we think about how we create code today, there's three main areas to focus on: @@ -50,8 +50,8 @@ Regardless of the reason, even seemingly innocuous tokens can create a security Let's enable Secret scanning to detect any potential keys. 1. On the same page (**Settings** > **Code security and analysis**), towards the very bottom, locate the **Secret scanning** section. -1. Next to **Receive alerts on GitHub for detected secrets, keys or other tokens**, select **Enable**. -1. Next to **Push protection**, select **Enable** to block pushes to the repository which contain a [supported secret][supported-secrets]. +2. Next to **Receive alerts on GitHub for detected secrets, keys or other tokens**, select **Enable**. +3. Next to **Push protection**, select **Enable** to block pushes to the repository which contain a [supported secret][supported-secrets]. ![Screenshot of fully configured secret scanning](./images/1-secret-scanning.png) @@ -84,7 +84,7 @@ A background process starts, and will configure a workflow for analyzing your co ## Summary and next steps -In this exercise, you enabled GitHub Advanced Security. You enabled Dependabot to check the libraries your project takes dependencies on, secret scanning to look for keys and tokens, and code scanning to examine your source code. These tools help ensure your application is secure. Next it's time to [file an issue][walkthrough-next] to add feature requests. +In this exercise, you enabled GitHub Advanced Security. You enabled Dependabot to check the libraries your project takes dependencies on, secret scanning to look for keys and tokens, and code scanning to examine your source code. These tools help ensure your application is secure. Next it's time to [file an issue][next] to add feature requests. ### Additional resources @@ -92,17 +92,18 @@ In this exercise, you enabled GitHub Advanced Security. You enabled Dependabot t - [GitHub Skills: Secure your repository's supply chain][skills-supply-chain] - [GitHub Skills: Secure code game][skills-secure-code] -| [← Workshop setup][walkthrough-previous] | [Next: Project management with GitHub Issues →][walkthrough-next] | +| [← Workshop setup][previous] | [Next: Project management with GitHub Issues →][next] | |:-----------------------------------|------------------------------------------:| +[next]: ./2-issues.md +[previous]: ./0-setup.md + +[about-code-scanning]: https://docs.github.com/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/about-code-scanning +[about-prs]: https://docs.github.com/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests [advanced-security]: https://github.com/features/security -[advanced-security-docs]: https://docs.github.com/en/get-started/learning-about-github/about-github-advanced-security -[about-code-scanning]: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/about-code-scanning -[about-prs]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests -[dependabot-quickstart]: https://docs.github.com/en/code-security/getting-started/dependabot-quickstart-guide +[advanced-security-docs]: https://docs.github.com/get-started/learning-about-github/about-github-advanced-security +[dependabot-quickstart]: https://docs.github.com/code-security/getting-started/dependabot-quickstart-guide [github-actions]: https://github.com/features/actions -[supported-secrets]: https://docs.github.com/en/code-security/secret-scanning/secret-scanning-patterns#supported-secrets -[skills-supply-chain]: https://github.com/skills/secure-repository-supply-chain [skills-secure-code]: https://github.com/skills/secure-code-game -[walkthrough-previous]: 0-setup.md -[walkthrough-next]: 2-issues.md +[skills-supply-chain]: https://github.com/skills/secure-repository-supply-chain +[supported-secrets]: https://docs.github.com/code-security/secret-scanning/secret-scanning-patterns#supported-secrets diff --git a/content/1-hour/0-setup.md b/content/1-hour/0-setup.md deleted file mode 100644 index 221e352..0000000 --- a/content/1-hour/0-setup.md +++ /dev/null @@ -1,86 +0,0 @@ -# Workshop setup - -| [← Getting started with GitHub Copilot][walkthrough-previous] | [Next: Coding with GitHub Copilot →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -To complete this workshop you will need to create a repository with a copy of the contents of this repository. While this can be done by [forking a repository][fork-repo], the goal of a fork is to eventually merge code back into the original (or upstream) source. In our case we want a separate copy as we don't intend to merge our changes. This is accomplished through the use of a [template repository][template-repo]. Template repositories are a great way to provide starters for your organization, ensuring consistency across projects. - -The repository for this workshop is configured as a template, so we can use it to create your repository. - -> [!IMPORTANT] -> Ensure you have the [requisite software][required-software] and [requisite resources][required-resources] setup. - -## Create your repository - -Let's create the repository you'll use for your workshop. - -1. Navigate to [the repository root](/) -2. Select **Use this template** > **Create a new repository** - - ![Screenshot of Use this template dropdown](images/0-setup-template.png) - -3. Under **Owner**, select the name of your GitHub handle, or the owner specified by your workshop leader. -4. Under **Repository**, set the name to **pets-workshop**, or the name specified by your workshop leader. -5. Ensure **Public** is selected for the visibility, or the value indicated by your workshop leader. -6. Select **Create repository from template**. - - ![Screenshot of configured template creation dialog](images/0-setup-configure.png) - -In a few moments a new repository will be created from the template for this workshop! - -## Clone the repository and start the app - -With the repository created, it's now time to clone the repository locally. We'll do this from a shell capable of running BASH commands. - -1. Copy the URL for the repository you just created in the prior set. -2. Open your terminal or command shell. -3. Run the following command to clone the repository locally (changing directories to a parent directory as appropriate): - - ```sh - git clone - ``` - -4. Change directories into the cloned repository by running the following command: - - ```sh - cd - ``` - -5. Start the application by running the following command: - - ```sh - ./scripts/start-app.sh - ``` - -The startup script will start two applications: - -- The backend Flask app on [localhost:5100][flask-url]. You can see a list of dogs by opening the [dogs API][dogs-api]. -- The frontend Astro/Svelte app on [localhost:4321][astro-url]. You can see the [website][website-url] by opening that URL. - -## Open your editor - -With the code cloned locally, and the site running, let's open the codebase up in VS Code. - -1. Open VS Code. -2. Select **File** > **Open Folder**. -3. Navigate to the folder which contains the project you cloned earlier in this exercise. -4. With the folder highlighted, select **Open folder**. - -## Summary and next steps - -You've now cloned the repository you'll use for this workshop and have your IDE setup! Next let's [add a new endpoint to the server][walkthrough-next]! - - -| [← Getting started with GitHub Copilot][walkthrough-previous] | [Next: Coding with GitHub Copilot →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[astro-url]: http://localhost:4321 -[dogs-api]: http://localhost:5100/api/dogs -[flask-url]: http://localhost:5100 -[fork-repo]: https://docs.github.com/en/get-started/quickstart/fork-a-repo -[required-resources]: ./README.md#required-resources -[required-software]: ./README.md#required-local-installation -[template-repo]: https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository -[walkthrough-previous]: README.md -[walkthrough-next]: ./1-add-endpoint.md -[website-url]: http://localhost:4321 \ No newline at end of file diff --git a/content/1-hour/1-add-endpoint.md b/content/1-hour/1-add-endpoint.md deleted file mode 100644 index 84fbbaf..0000000 --- a/content/1-hour/1-add-endpoint.md +++ /dev/null @@ -1,106 +0,0 @@ -# Coding with GitHub Copilot - -| [← Workshop setup][walkthrough-previous] | [Next: Helping GitHub Copilot understand context →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - - -With code completions, GitHub Copilot provides suggestions in your code editor while you're coding. This can turn comments into code, generate the next line of code, and generate an entire function just from a signature. Code completion helps reduce the amount of boilerplate code and ceremony you need to type, allowing you to focus on the important aspects of what you're creating. - -## Scenario - -It's standard to work in phases when adding functionality to an application. Given that we know we want to allow users to filter the list of dogs based on breed, we'll need to add an endpoint to provide a list of all breeds. Later we'll add the rest of the functionality, but let's focus on this part for now. - -The application uses a Flask app with SQLAlchemy as the backend API (in the [/server][server-code] folder), and an Astro app with Svelte as the frontend (in the [/client][client-code] folder). You will explore more of the project later; this exercise will focus solely on the Flask application. - -> [!NOTE] -> As you begin making changes to the application, there is always a chance a breaking change could be created. If the page stops working, check the terminal window you used previously to start the application for any error messages. You can stop the app by using Ctl+C, and restart it by running `./scripts/start-app.sh`. - -## Flask routes - -While we won't be able to provide a full overview of [routing in Flask][flask-routing], they are defined by using the Python decorator `@app.route`. There are a couple of parameters you can provide to `@app.route`, including the path (or URL) one would use to access the route (such as **api/breeds**), and the [HTTP method(s)][http-methods] which can be used. - -## Code completion - -Code completion predicts the next block of code you're about to type based on the context Copilot has. For code completion, this includes the file you're currently working on and any tabs open in your IDE. - -Code completion is best for situations where you know what you want to do, and are more than happy to just start writing code with a bit of a helping hand along the way. Suggestions will be generated based both on the code you write (say a function definition) and comments you add to your code. - -## Create the breeds endpoint - -Let's build our new route in our Flask backend with the help of code completion. - -> [!IMPORTANT] -> For this exercise, **DO NOT** copy and paste the code snippet provided, but rather type it manually. This will allow you to experience code completion as you would if you were coding back at your desk. You'll likely see you only have to type a few characters before GitHub Copilot begins suggesting the rest. - -1. Return to your IDE with the project open. -2. Open **server/app.py**. -3. Locate the comment which reads `## HERE`, which should be at line 68. -4. Delete the comment to ensure there isn't any confusion for Copilot, and leave your cursor there. -5. Begin adding the code to create the route to return all breeds from an endpoint of **api/breeds** by typing the following: - - ```python - @app.route('/api/breeds', methods=['GET']) - ``` - -6. Once you see the full function signature, select Tab to accept the code suggestion. -7. If it didn't already, code completion should then suggest the remainder of the function signature; just as before select Tab to accept the code suggestion. - - The code generated should look a little like this: - - ```python - @app.route('/api/breeds', methods=['GET']) - def get_breeds(): - # Query all breeds - breeds_query = db.session.query(Breed.id, Breed.name).all() - - # Convert the result to a list of dictionaries - breeds_list = [ - { - 'id': breed.id, - 'name': breed.name - } - for breed in breeds_query - ] - - return jsonify(breeds_list) - ``` - -> [!IMPORTANT] -> Because LLMs are probabilistic, not deterministic, the exact code generated can vary. The above is a representative example. If your code is different, that's just fine as long as it works! - -8. Add a comment to the newly created function. To do this, place your cursor inside the function (anywhere between the lines `def get_breeds...` and `return jsonify...`). Then, press Ctl+I (or cmd+I on a Mac) to open the editor inline chat. In the input box, type `/doc`. (You can optionally provide additional details, but it's not required). This will prompt GitHub Copilot to generate a documentation comment for the function. The suggested comment will appear inline in the code (highlighted in green). Click **Accept** to apply the comment to your code, or click **Close** to discard the suggestion. You just used a slash command, a shortcut to streamline a task, these commands eliminate the need for verbose prompts. - -9. **Save** the file. - -## Validate the endpoint - -With the code created and saved, let's quickly validate the endpoint to ensure it works. - -1. Navigate to [http://localhost:5100/api/breeds][breeds-endpoint] to validate the route. You should see JSON displayed which contains the list of breeds! - -## Summary and next steps - -You've added a new endpoint with the help of GitHub Copilot! You saw how Copilot predicted the next block of code you were likely looking for and provided the suggestion inline, helping save you the effort of typing it out manually. Let's start down the path of performing more complex operations by [exploring our project][walkthrough-next]. - -## Resources - -- [Code suggestions in your IDE with GitHub Copilot][copilot-suggestions] -- [Code completions with GitHub Copilot in VS Code][vscode-copilot] -- [Prompt crafting][prompt-crafting] -- [Inline chat][inline-chat] - - -| [← Workshop setup][walkthrough-previous] | [Next: Helping GitHub Copilot understand context →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[breeds-endpoint]: http://localhost:5100/api/breeds -[client-code]: /client/ -[copilot-suggestions]: https://docs.github.com/en/copilot/using-github-copilot/getting-code-suggestions-in-your-ide-with-github-copilot -[flask-routing]: https://flask.palletsprojects.com/en/stable/quickstart/#routing -[http-methods]: https://www.w3schools.com/tags/ref_httpmethods.asp -[prompt-crafting]: https://code.visualstudio.com/docs/copilot/prompt-crafting -[inline-chat]: https://code.visualstudio.com/docs/copilot/chat/inline-chat -[server-code]: /server/ -[vscode-copilot]: https://code.visualstudio.com/docs/copilot/ai-powered-suggestions -[walkthrough-previous]: ./0-setup.md -[walkthrough-next]: ./2-explore-project.md \ No newline at end of file diff --git a/content/1-hour/2-explore-project.md b/content/1-hour/2-explore-project.md deleted file mode 100644 index 5e73d18..0000000 --- a/content/1-hour/2-explore-project.md +++ /dev/null @@ -1,49 +0,0 @@ -# Helping GitHub Copilot understand context - -| [← Coding with GitHub Copilot][walkthrough-previous] | [Next: Providing custom instructions →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -The key to success when coding (and much of life) is context. Before we add code to a codebase, we want to understand the rules and structures already in place. When working with an AI coding assistant such as GitHub Copilot the same concept applies - the quality of suggestion is directly proportional to the context Copilot has. Let's use this opportunity to both explore the project we've been given and how to interact with Copilot to ensure it has the context it needs to do its best work. - -## Scenario - -Before adding new functionality to the website, you want to explore the existing structure to determine where the updates need to be made. - -## Chat participants and extensions - -GitHub Copilot Chat has a set of available [chat participants][chat-participants] and [extensions][copilot-extensions] available to you to both provide instructions to GitHub Copilot and access external services. Chat participants are helpers which work inside your IDE and have access to your project, while extensions can call external services and provide information to you without having to open separate tools. We're going to focus on one core chat participant - `@workspace`. - -`@workspace` creates an index of your project and allows you to ask questions about what you're currently working on, to find resources inside the project, or add it to the context. It's best to use this when the entirety of your project should be considered or you're not entirely sure where you should start looking. In our current scenario, since we want to ask questions about our project, `@workspace` is the perfect tool for the job. - -> [!NOTE] -> This exercise doesn't provide specific prompts to type, as part of the learning experience is to discover how to interact with Copilot. Feel free to talk in natural language, describing what you're looking for or need to accomplish. - -1. Return to your IDE with the project open. -2. Close any tabs you may have open in your IDE to ensure the context for Copilot chat is empty. -3. Open GitHub Copilot Chat. -4. Select the `+` icon towards the top of Copilot chat to begin a new chat. -5. Type `@workspace` in the chat prompt window and hit tab to select or activate it, then continue by asking Copilot about your project. You can ask what technologies are in use, what the project does, where functionality resides, etc. -6. Spend a few minutes exploring to find the answers to the following questions: - - Where's the database the project uses? - - What files are involved in listing dogs? - -## Summary and next steps - -You've explored context in GitHub Copilot, which is key to generating quality suggestions. You saw how you can use chat participants to help guide GitHub Copilot, and how with natural language you can explore the project. Let's see how we can provide even more context to Copilot chat through the use of [Copilot instructions][walkthrough-next]. - -## Resources - -- [Copilot Chat cookbook][copilot-cookbook] -- [Use Copilot Chat in VS Code][copilot-chat-vscode] -- [Copilot extensions marketplace][copilot-marketplace] - -| [← Coding with GitHub Copilot][walkthrough-previous] | [Next: Providing custom instructions →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[chat-participants]: https://code.visualstudio.com/docs/copilot/copilot-chat#_chat-participants -[copilot-chat-vscode]: https://code.visualstudio.com/docs/copilot/copilot-chat -[copilot-cookbook]: https://docs.github.com/en/copilot/copilot-chat-cookbook -[copilot-extensions]: https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat -[copilot-marketplace]: https://github.com/marketplace?type=apps&copilot_app=true -[walkthrough-previous]: ./1-add-endpoint.md -[walkthrough-next]: ./3-copilot-instructions.md \ No newline at end of file diff --git a/content/1-hour/3-copilot-instructions.md b/content/1-hour/3-copilot-instructions.md deleted file mode 100644 index b9b07ff..0000000 --- a/content/1-hour/3-copilot-instructions.md +++ /dev/null @@ -1,115 +0,0 @@ -# Providing custom instructions - -| [← Coding with GitHub Copilot][walkthrough-previous] | [Next: Add the filter feature →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -There are always key pieces of information anyone generating code for your codebase needs to know - the technologies in use, coding standards to follow, project structure, etc. Since context is so important, as we've discussed, we likely want to ensure Copilot always has this information as well. Fortunately, we can provide this overview through the use of Copilot instructions. - -## Scenario - -Before we begin larger updates to the site with the help of Copilot, we want to ensure Copilot has a good understanding of how we're building our application. As a result, we're going to add a Copilot instructions file to the repository. - -## Overview of Copilot instructions - -Copilot instructions is a markdown file is placed in your **.github** folder. It becomes part of your project, and in turn to all contributors to your codebase. You can use this file to indicate various coding standards you wish to follow, the technologies your project uses, or anything else important for Copilot Chat to understand when generating suggestions. - -> [!IMPORTANT] -> The *copilot-instructions.md* file is included in **every** call to GitHub Copilot Chat, and will be part of the context sent to Copilot. Because there is always a limited set of tokens an LLM can operate on, a large Copilot instructions file can obscure relevant information. As such, you should limit your Copilot instructions file to project-wide information, providing an overview of what you're building and how you're building it. If you need to provide more specific information for particular tasks, you can create [prompt files](https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot?tool=vscode#about-prompt-files). - -Here are some guidelines to consider when creating a Copilot instructions file: - -- The Copilot instructions file becomes part of the project, meaning it will apply to every developer; anything indicated in the file should be globally applicable. -- The file is markdown, so you can take advantage of that fact by grouping content together to improve readability. -- Provide overview of **what** you are building and **how** you are building it, including: - - languages, frameworks and libraries in use. - - required assets to be generated (such as unit tests) and where they should be placed. - - any language specific rules such as: - - utilize [type hints][type-hints] in Python. - - use [arrow functions][arrow-functions] rather than the `function` keyword in TypeScript. -- If you notice GitHub Copilot consistently provides an unexpected suggestion (e.g. using class components for React), add those notes to the instructions file. - -## Create a Copilot instructions file - -Let's create a Copilot instructions file. We'll start by asking Copilot to generate a block of code, then add the instructions file, then ask the same question again to see the changes. - -1. Return to your IDE with your project open. -2. Close any tabs you may have open in your IDE to ensure Copilot chat has an empty context. -3. Select the `+` icon towards the top of Copilot chat to begin a new chat. -4. Open Copilot Chat and send the following prompt: - - ``` - Create a Python function to validate dog age. Ensure age is between 0 and 20. Throw an error if it is outside this range. - ``` - -5. Note the function signature is similar to `def validate_dog_age(age)` without type hints. - -> [!NOTE] -> Because LLMs are probabilistic rather than deterministic, the exact code will vary. - -6. Create a new file in the **.github** folder called **copilot-instructions.md**. -7. Add the markdown to the file necessary which provides information about the project structure and requirements: - - ```markdown - # Dog shelter - - This is an application to allow people to look for dogs to adopt. It is built in a monorepo, with a Flask-based backend and Astro-based frontend. - - ## Backend - - - Built using Flask and SQLAlchemy - - Use type hints - - ## Frontend - - - Built using Astro and Svelte - - TypeScript should use arrow functions rather than the function keyword - - Pages should be in dark mode with a modern look and feel - ``` - -8. **Save** the file. - -## Watch the instructions file in action - -Whenever you make a call to Copilot chat, the references dialog indicates all files used to generate the response. Once you create a Copilot instructions file, you will see it's always included in the references section. Since you included directions to use type hints, you'll notice the code suggestions will follow this guidance. - -1. Close all files currently open in VS Code or your Codespace. (This will ensure we are working with an empty context.) -2. Select the `+` icon in GitHub Copilot chat to start a new chat. -3. Send Copilot chat the same prompt you used previously: - - ``` - Create a Python function to validate dog age. Ensure age is between 0 and 20. Throw an error if it is outside this range. - ``` - -> [!TIP] -> You can use up arrow to resend previous prompts to Copilot chat. - -4. Note the references now includes the instructions file and provides information gathered from it. - - ![Screenshot of the chat window with the references section expanded displaying Copilot instructions in the list](./images/copilot-chat-references.png) - -5. Note the resulting Python now utilizes type hints, and the function signature will resemble the following: - - ```python - def validate_dog_age(age: int): - ``` - -> [!NOTE] -> The exact code generated will vary, but the new Python suggestion should now utilize type hints. - -## Summary and next steps - -Copilot instructions improves the quality of suggestions, and ensures better alignment with the desired practices you have in place. With the groundwork in place, let's [add new functionality to our website][walkthrough-next]! - -## Resources - -- [Adding repository custom instructions for GitHub Copilot][custom-instructions] - - -| [← Coding with GitHub Copilot][walkthrough-previous] | [Next: Add the filter feature →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[arrow-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions -[custom-instructions]: https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot -[type-hints]: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html -[walkthrough-previous]: ./2-explore-project.md -[walkthrough-next]: ./4-add-feature.md \ No newline at end of file diff --git a/content/1-hour/4-add-feature.md b/content/1-hour/4-add-feature.md deleted file mode 100644 index 52e3efc..0000000 --- a/content/1-hour/4-add-feature.md +++ /dev/null @@ -1,103 +0,0 @@ -# Add the filter feature - -| [← Providing custom instructions][walkthrough-previous] | [Next: Bonus content →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -We've explored how we can use GitHub Copilot to explore our project and to provide context to ensure the suggestions we receive are to the quality we expect. Now let's turn our attention to putting all this prep work into action by generating new code! We'll use GitHub Copilot to aid us in adding functionality to our website. - -## Scenario - -The website currently lists all dogs in the database. While this was appropriate when the shelter only had a few dogs, as time has gone on the number has grown and it's difficult for people to sift through who's available to adopt. The shelter has asked you to add filters to the website to allow a user to select a breed of dog and only display dogs which are available for adoption. - -## Copilot Edits - -Previously we utilized Copilot chat, which is great for working with an individual file or asking questions about our code. However, many updates necessitate changes to multiple files throughout a codebase. Even a seemingly basic change to a webpage likely requires updating HTML, CSS and JavaScript files. Copilot Edits allows you to modify multiple files at once. - -With Copilot Edits, you will add the files which need to be updated to the context. Once you provide the prompt, Copilot Edits will begin the updates across all files in the context. It also has the ability to create new files or add files to the context as it deems appropriate. - -## Add the filters to the dog list page - -Adding the filters to the page will require updating a minimum of two files - the Flask backend and the Svelte frontend. Fortunately, Copilot Edits can update multiple files! Let's get our page updated with the help of Copilot Edits. - -> [!NOTE] -> Because Copilot Edits works best with auto-save enabled, we'll activate it. As we'll explore a little later in this exercise, Copilot Edits provides powerful tools to undo any changes you might not wish to keep. - -1. Return to your IDE with your project open. -2. Close any tabs you have open inside your IDE. -3. Enable Auto Save by selecting **File** > **Auto Save**. -4. Open GitHub Copilot Chat. -5. Switch to edit mode by selecting **Edit** in the chat mode dropdown at the bottom of Chat view (should be currently **Ask**) -6. If available, select **Claude 3.5 Sonnet** from the list of available models -7. Select **Add Context...** in the chat window. -8. Select **server/app.py** and **client/src/components/DogList.svelte** files (you need to select **Add context** for each file) -> [!TIP] -> If you type the file names after clicking **Add context**, they will show up in the filter. You can also drag the files or right click file in explorer and select `Copilot -> Add File to Chat`) -9. Ask Copilot to generate the update you want to the page, which is to add filters for both dog breed and if dogs are available for adoption. Use your own phrasing, ensuring the following requirements are met: - - A dropdown list should be provided with all breeds - - A checkbox should be available to only show available dogs - - The page should automatically refresh whenever a change is made - -> [!NOTE] -> You should use your own phrasing when generating the prompt. As highlighted previously, part of the exercise is to become comfortable creating prompts for GitHub Copilot. One key tip is it's always good to provide more guidance to ensure you get the code you are looking for. - -Copilot begins generating the suggestions! - -## Reviewing the suggestions - -Unlike our prior examples where we worked with an individual file, we're now working with changes across multiple files - and maybe multiple sections of multiple files. Fortunately, Copilot Edits has functionality to help streamline this process. - -GitHub Copilot will propose the following changes: - -- Update the endpoint to list all dogs to accept parameters for breed and availability. -- Update the webpage to include the dropdown list and checkbox. - -As the code is generated, you will notice the files are displayed using an experience similar to diff files, with the new code highlighted in green and old code highlighted in red (by default). - -If you open an individual file, you can keep or undo changes by using the buttons provided. - -![Screenshot of keep/undo interface for an individual file](./images/copilot-edits-keep-undo-file.png) - -You can also keep or undo all changes made. - -![Screenshot of keep/discard interface on the chat window](./images/copilot-edits-keep-undo-global.png) - -And - -1. Review the code suggestions to ensure they behave the way you expect them to, making any necessary changes. Once you're satisfied, you can select **Keep** on the files individually or in Copilot Chat to accept all changes. -2. Open the page at [http://localhost:4321][tailspin-shelter-website] to see the updates! -3. Run the Python tests by using `python -m unittest` in the terminal as you did previously. -4. If any changes are needed, explain the required updates to GitHub Copilot and allow it to generate the new code. - -> [!IMPORTANT] -> Working iteratively a normal aspect of coding with an AI pair programmer. You can always provide more context to ensure Copilot understands, make additional requests, or rephrase your original prompts. To aid you in working iteratively, you will notice undo and redo buttons towards the top of the Copilot Edits interface, which allow you to move back and forth across prompts. -> -> ![Screenshot of the undo/redo buttons](./images/copilot-edits-history.png) - -5. Confirm the functionality works as expected, then select **Keep** to accept all the changes. -6. Optional: Disable Auto Save by unselecting **File** > **Auto Save**. - -## Summary - -You've worked with GitHub Copilot to add new features to the website - the ability to filter the list of dogs. With the help of Copilot Edits, you updated multiple files across the project, and iteratively built the desired functionality. - -## Workshop review - -Over the course of the workshop you explore the core functionality of GitHub Copilot. You saw how to use code completion to get inline suggestions, chat participants to explore your project, Copilot instructions to add context, and Copilot Edits to update multiple files. - -There is no one right way to use GitHub Copilot. Continue to explore and try different prompts to discover what works best for your workflow and how GitHub Copilot can aid your productivity. - -## Resources - -- [Asking GitHub Copilot questions in your IDE][copilot-ask] -- [Copilot Chat cookbook][copilot-cookbook] -- [Copilot Edits][copilot-edits] - -| [← Providing custom instructions][walkthrough-previous] | [Next: Bonus content →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[copilot-ask]: https://docs.github.com/en/copilot/using-github-copilot/copilot-chat/asking-github-copilot-questions-in-your-ide -[copilot-cookbook]: https://docs.github.com/en/copilot/copilot-chat-cookbook -[copilot-edits]: https://code.visualstudio.com/docs/copilot/copilot-edits -[tailspin-shelter-website]: http://localhost:4321 -[walkthrough-previous]: ./3-copilot-instructions.md -[walkthrough-next]: ./5-bonus.md diff --git a/content/1-hour/5-bonus.md b/content/1-hour/5-bonus.md deleted file mode 100644 index de66dcd..0000000 --- a/content/1-hour/5-bonus.md +++ /dev/null @@ -1,84 +0,0 @@ -# Bonus content - -| [← Add the filter feature][walkthrough-previous] | [Next: Pets workshop selection →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -## Overview of Copilot Agent Mode - -With chat agent mode in Visual Studio Code, you can use natural language to define a high-level task and to start an agentic code editing session to accomplish that task. In agent mode, Copilot **autonomously** plans the work needed and determines the relevant files and context. It then makes edits to your codebase and invokes tools to accomplish the request you made. Agent mode monitors the outcome of edits and tools and iterates to resolve any issues that arise. - -> [!IMPORTANT] -> While Copilot autonomously determines the operations necessary to complete the requested task, as the developer you are always in charge. You will work with Copilot to ensure everything is completely correctly, reading and reviewing the code. You will also want to continue to follow proper DevOps practices, including code reviews, testing, security scans, etc. - -Why would you use agent mode instead of edit mode? - -- **Edit scope**: agent mode autonomously determines the relevant context and files to edit. In edit mode, you need to specify the context yourself. -- **Task complexity**: agent mode is better suited for complex tasks that require not only code edits but also the invocation of tools and terminal commands. -- **Duration**: agent mode involves multiple steps to process a request, so it might take longer to get a response. For example, to determine the relevant context and files to edit, determine the plan of action, and more. -- **Self-healing**: agent mode evaluates the outcome of the generated edits and might iterate multiple times to resolve intermediate issues. -- **Request quota**: in agent mode, depending on the complexity of the task, one prompt might result in many requests to the backend. - -### How it works - -![How agent mode works](./images/copilot-agent-mode-how-it-works.png) - -## Add themes to the Tailspin Shelter website - -In this section, you will use Copilot's agent mode to add themes to the Tailspin Shelter website. You will be able to select a theme and apply it to the website. - -1. Return to your IDE with the project open. -2. Close any tabs you may have open in your IDE to ensure the context for Copilot chat is empty. -3. Select the `+` icon towards the top of Copilot chat to begin a new chat. -4. Select agent mode, by selecting `Agent` (just like you did `Edit` before) in the model selector dropdown at the bottom of the chat window. -5. Select one of models (some may not be available) `Claude 3.7 Sonnet`, `Claude 3.5 Sonnet` or `GPT-4.1 (Preview)` -6. Navigate to [](../prompts/fun-add-themes.md) -7. Copy the content of the prompt -8. Paste the content in the copilot prompt input -9. The agent mode will take its time, since it searches by itself the relevant files to modify, and then do multiple passes including talking with itself to refine the task at hand -10. While Agent is doing it's thing, take the opportunity to examine the content of prompt that was used. -11. When the agent is done (you no longer see any spinners and the thumb up/down icons will be visible), open a browser to see the results - - Open the page at [http://localhost:4321][tailspin-shelter-website] to see the updates! - - Examine the changes made to the files if you like - - Was it good? If you are not happy with the results, you can refine the prompt by crafting extra prompts in the chat to improve the end results. Don't start a new session, it's an interactive process. -12. Press `Done` when you are happy with the results - -You _may_ have gotten something like this for the Terminal Theme (generated with claude 3.7) - -![Tailspin Shelter Terminal Classic theme](images/tail-spin-shelter-terminal-theme.png) - -> [!IMPORTANT] -> Because LLMs are probabilistic, not deterministic, the exact code generated can vary. The above is a representative example. If your code is different, that's just fine as long as it works! - -## Play a bit with Copilot - -You've made it to the end of the one hour workshop. Congratulations! You've explored the core skills to help you get the most out of GitHub Copilot. From here you can explore various challenges on your own, and see how GitHub Copilot can support you as you continue developing. - -The suggestions listed here are exactly that - suggestions. You're free to come up with your own scenarios or features you think the application should have. - -You'll also notice there aren't step-by-step instructions here. You've already seen how you can use Copilot to aid you in development. Part of the challenge put forth with these extra suggestions is to apply what you've learned to create code! - -### Some prompts to play with - -We have provided you some prompts in [prompts][github-prompts-path] folder, which you can use directly as inspiration for your explorations. - -> [!TIP] -> These prompts are meant to be used as one shot, but if have prompts that can be are generic, reusable prompt are a great way to share prompts with the team members. They can be placed in a well know folder and be invoked directly in the Copilot Chat by referencing them. -> Learn more about [reusable prompts in Visual Studio Code][vscode-prompts] - -### Potential next steps - -Here's some ideas of how you could continue to grow and build upon what you've done: - -- Return to the API endpoints you updated previously in Flask and add unit tests. -- Add paging support to the full list of dogs or any results page with more than 5 results. -- Add a form to allow a user to apply to adopt a dog if the dog is available. -- Add a form to allow users to register a dog they found. - -| [← Add the filter feature][walkthrough-previous] | [Next: Pets workshop selection →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[walkthrough-previous]: ./4-add-feature.md -[walkthrough-next]: ../README.md -[tailspin-shelter-website]: http://localhost:4321 -[github-prompts-path]: ../prompts/ -[vscode-prompts]: https://aka.ms/vscode-ghcp-prompt-snippets \ No newline at end of file diff --git a/content/1-hour/README.md b/content/1-hour/README.md deleted file mode 100644 index 28a5d94..0000000 --- a/content/1-hour/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Getting started with GitHub Copilot - -| [← Pets workshop selection][walkthrough-previous] | [Next: Workshop setup →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -Built to be your AI pair programmer, [GitHub Copilot][copilot] helps you generate code and focus on what's important. Through the use of code completion you can create code from comments, and functions from just a signature. With Copilot chat you can ask questions about your codebase, create new files and update existing ones, and even perform operations which update files across your entire codebase. - -As with any tool, there are a set of skills which need to be acquired, which is the purpose of this (roughly) one hour workshop. You'll explore the most common workloads available to you by exploring and updating an existing application to add functionality. - -## Prerequisites - -The application for the workshop uses is built primarily with Python (Flask and SQLAlchemy) and Astro (using Tailwind and Svelte). While experience with these frameworks and languages is helpful, you'll be using Copilot to help you understand the project and generate the code. As a result, as long as you are familiar with programming you'll be able to complete the exercises! - -> [!NOTE] -> When in doubt, you can always highlight a block of code you're unfamiliar with and ask GitHub Copilot chat for an explanation! - -## Required resources - -To complete this workshop, you will need the following: - -- A [GitHub account][github-account]. -- Access to [GitHub Copilot][copilot] (which is available for free for individuals!) - -## Required local installation - -You will also need the following available and installed locally: - -### Code editor - -- [Visual Studio Code][vscode-link]. -- [Copilot extension installed in your IDE][copilot-extension]. - -### Local services - -- A recent [Node.js runtime][nodejs-link]. -- A recent version of [Python][python-link]. - - For Windows, you can install [Python via the Windows store](https://apps.microsoft.com/detail/9pjpw5ldxlz5?hl=en-US&gl=US). -- The [git CLI][git-link]. -- A shell capable of running BASH commands. - -> [!NOTE] -> Linux and macOS are able to run BASH commands without additional configuration. For Windows, you will need either [Windows Subsystem for Linux (WS)][windows-subsystem-linux] or the BASH shell available via [git][git-link]. - -## Getting started - -Ready to get started? Let's go! The workshop scenario imagines you as a developer volunteering your time for a pet adoption center. You've been asked to add a filter to the website to allow people to limit their search results by breed and adoption status. You'll work over the next 5 exercises to perform the tasks! - -0. [Clone the repository and start the app][walkthrough-next] for the workshop. -1. [Add an endpoint to the server][stage-1] to list all breeds. -2. [Explore the project][stage-2] to get a better understanding of what needs to be done. -3. [Create custom instructions][stage-3] to ensure Copilot chat has additional context. -4. [Add the new feature][stage-4] to the website, and ensure it works! - -## Check out these resources to dive in and learn more -Check out the resources in [**GitHub-Copilot-Resources.md**][GitHub-Copilot-Resources]. - -This resource list has been carefully curated to help you to learn more about GitHub Copilot, how to use it effectively, what is coming in the future and more. There are even YouTube playlists that include the latest videos from the GitHub Developer Relations team and others from GitHub. - -| [← Pets workshop selection][walkthrough-previous] | [Next: Workshop setup →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[copilot]: https://github.com/features/copilot -[copilot-extension]: https://docs.github.com/en/copilot/managing-copilot/configure-personal-settings/installing-the-github-copilot-extension-in-your-environment -[git-link]: https://git-scm.com/ -[github-account]: https://github.com/join -[nodejs-link]: https://nodejs.org/en -[python-link]: https://www.python.org/ -[stage-1]: ./1-add-endpoint.md -[stage-2]: ./2-explore-project.md -[stage-3]: ./3-copilot-instructions.md -[stage-4]: ./4-add-feature.md -[walkthrough-previous]: ../README.md -[walkthrough-next]: ./0-setup.md -[windows-python-link]: https://apps.microsoft.com/detail/9pjpw5ldxlz5 -[windows-subsystem-linux]: https://learn.microsoft.com/en-us/windows/wsl/about -[vscode-link]: https://code.visualstudio.com/ -[GitHub-Copilot-Resources]: ../GitHub-Copilot-Resources.md diff --git a/content/1-hour/images/0-setup-configure.png b/content/1-hour/images/0-setup-configure.png deleted file mode 100644 index e6810be..0000000 Binary files a/content/1-hour/images/0-setup-configure.png and /dev/null differ diff --git a/content/1-hour/images/0-setup-template.png b/content/1-hour/images/0-setup-template.png deleted file mode 100644 index 98b6918..0000000 Binary files a/content/1-hour/images/0-setup-template.png and /dev/null differ diff --git a/content/1-hour/images/copilot-agent-mode-how-it-works.png b/content/1-hour/images/copilot-agent-mode-how-it-works.png deleted file mode 100644 index b026a6c..0000000 Binary files a/content/1-hour/images/copilot-agent-mode-how-it-works.png and /dev/null differ diff --git a/content/1-hour/images/copilot-chat-references.png b/content/1-hour/images/copilot-chat-references.png deleted file mode 100644 index a96d877..0000000 Binary files a/content/1-hour/images/copilot-chat-references.png and /dev/null differ diff --git a/content/1-hour/images/copilot-edits-history.png b/content/1-hour/images/copilot-edits-history.png deleted file mode 100644 index fdc2b8c..0000000 Binary files a/content/1-hour/images/copilot-edits-history.png and /dev/null differ diff --git a/content/1-hour/images/copilot-edits-keep-undo-file.png b/content/1-hour/images/copilot-edits-keep-undo-file.png deleted file mode 100644 index 3f52bcc..0000000 Binary files a/content/1-hour/images/copilot-edits-keep-undo-file.png and /dev/null differ diff --git a/content/1-hour/images/copilot-edits-keep-undo-global.png b/content/1-hour/images/copilot-edits-keep-undo-global.png deleted file mode 100644 index a722a28..0000000 Binary files a/content/1-hour/images/copilot-edits-keep-undo-global.png and /dev/null differ diff --git a/content/1-hour/images/tail-spin-shelter-terminal-theme.png b/content/1-hour/images/tail-spin-shelter-terminal-theme.png deleted file mode 100644 index 3ab0920..0000000 Binary files a/content/1-hour/images/tail-spin-shelter-terminal-theme.png and /dev/null differ diff --git a/content/full-day/2-issues.md b/content/2-issues.md similarity index 70% rename from content/full-day/2-issues.md rename to content/2-issues.md index 70bbfa1..4285d7f 100644 --- a/content/full-day/2-issues.md +++ b/content/2-issues.md @@ -1,9 +1,9 @@ # Project management with GitHub Issues -| [← Securing the development pipeline][walkthrough-previous] | [Next: Cloud-based development with GitHub Codespaces →][walkthrough-next] | +| [← Securing the development pipeline][previous] | [Next: Cloud-based development with GitHub Codespaces →][next] | |:-----------------------------------|------------------------------------------:| -"URL or it didn't happen" is a common mantra at GitHub, which is used to highlight the importance of documenting the development process. Feature requests should have a history; who made the request, what was the rationale, who was involved in the process, what decisions were made, why were they made, was the feature implemented, how was it implemented... All of this information helps provide context to both drive future decisions and avoid repeating old mistakes. +"URL or it didn't happen" is a common mantra at GitHub, which is used to highlight the importance of documenting the development process. Feature requests should have a history: who made the request, what was the rationale, who was involved in the process, what decisions were made, why were they made, was the feature implemented, how was it implemented... All of this information helps provide context to both drive future decisions and avoid repeating old mistakes. GitHub provides various features to enable collaboration and project management, including [GitHub Discussions][discussions], [wikis][wikis], [pull requests][about-prs] and [GitHub Issues][issues]. Each of these can help your organization drive the creation process. We're going to focus on GitHub Issues, which is the foundation of project management on GitHub. Issues can also be linked to [milestones](https://docs.github.com/issues/using-labels-and-milestones-to-track-work/about-milestones) and [Projects](https://docs.github.com/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects), helping organize them into a broad roadmap. @@ -33,20 +33,20 @@ The shelter wants to begin pushing new features to the website. They want to sta ## Creating issues to manage feature requests -Our project needs two main updates. We want to make the updates to support development for our project, and add a new component to the website to display the shelter's hours. Let's create the issues for each of these. In the next few exercises we'll begin making the appropriate updates to our project to resolve these requests. +Our project needs two main updates. We want to make the updates to support development for our project, and add a flag showing availability for users to find the right dog. Let's create the issues for each of these. In the next few exercises we'll begin making the appropriate updates to our project to resolve these requests. 1. Return to the repository you created at the beginning of this workshop. -1. Select the **Issues** tab. -1. Select **New issue**. -2. If prompted for type, select **Blank issue**. -3. Select **Create more** at the bottom of the page to streamline the creation process. -4. Create new issues by adding the information indicated in the table below, selecting **Submit new issue** after creating each one: - - | Title | Description | - | ----------------------- | ------------------------------------------------------------------------------ | - | Define codespace | Create the necessary definitions for the codespace to enable cloud development | - | Implement testing | Create a workflow to automate testing for continuous integration | - | Add filters to dog list | Add the code to allow users to filter for dogs by breed and availability | +2. Select the **Issues** tab. +3. Select **New issue**. +4. If prompted for type, select **Blank issue**. +5. Select **Create more** at the bottom of the page to streamline the creation process. +6. Create new issues by adding the information indicated in the table below, selecting **Submit new issue** after creating each one: + + | Title | Description | + | --------------------------------- | ------------------------------------------------------------------------------------------------------- | + | Define codespace | Create the necessary definitions for the codespace to enable cloud development | + | Implement testing | Create a workflow to automate testing for continuous integration | + | Add availability flag to dog list | Add the necessary code to display a flag for the status of each dog, and the tests for the new behavior | > [!TIP] > You can also save an issue by pressing Ctl - Enter (or Cmd - Return on a Mac) in the title or description fields. @@ -54,22 +54,25 @@ Our project needs two main updates. We want to make the updates to support devel You've now defined all the issues for the workshop! You'll use these issues to help guide your progress through the workshop. ## Summary and next steps -GitHub Issues are the core to project management on GitHub. Their flexibility allows your organization to determine the best course of action to support your development lifecycle's methodology. With your issues created, it's time to turn your attention to the first big change to the project, [defining a codespace][walkthrough-next]. + +GitHub Issues are the core to project management on GitHub. Their flexibility allows your organization to determine the best course of action to support your development lifecycle's methodology. With your issues created, it's time to turn your attention to the first big change to the project, [defining a codespace][next]. ## Resources + - [GitHub Issues][issues-docs] - [Communicate using markdown][skills-markdown] - [GitHub Projects][projects-docs] -| [← Securing the development pipeline][walkthrough-previous] | [Next: Cloud-based development with GitHub Codespaces →][walkthrough-next] | +| [← Securing the development pipeline][previous] | [Next: Cloud-based development with GitHub Codespaces →][next] | |:-----------------------------------|------------------------------------------:| +[next]: ./3-codespaces.md +[previous]: ./1-code-scanning.md + +[about-prs]: https://docs.github.com/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests [discussions]: https://github.com/features/discussions -[wikis]: https://docs.github.com/en/communities/documenting-your-project-with-wikis/about-wikis -[about-prs]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests [issues]: https://github.com/features/issues -[issues-docs]: https://docs.github.com/en/issues/tracking-your-work-with-issues/about-issues -[projects-docs]: https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/quickstart-for-projects +[issues-docs]: https://docs.github.com/issues/tracking-your-work-with-issues/about-issues +[projects-docs]: https://docs.github.com/issues/planning-and-tracking-with-projects/learning-about-projects/quickstart-for-projects [skills-markdown]: https://github.com/skills/communicate-using-markdown -[walkthrough-next]: 3-codespaces.md -[walkthrough-previous]: 1-code-scanning.md \ No newline at end of file +[wikis]: https://docs.github.com/communities/documenting-your-project-with-wikis/about-wikis \ No newline at end of file diff --git a/content/full-day/3-codespaces.md b/content/3-codespaces.md similarity index 69% rename from content/full-day/3-codespaces.md rename to content/3-codespaces.md index ed5538c..9233e44 100644 --- a/content/full-day/3-codespaces.md +++ b/content/3-codespaces.md @@ -1,9 +1,11 @@ # Cloud-based development with GitHub Codespaces -| [← Project management with GitHub Issues][walkthrough-previous] | [Next: Continuous integration and testing →][walkthrough-next] | +| [← Project management with GitHub Issues][previous] | [Next: Continuous integration and testing →][next] | |:-----------------------------------|------------------------------------------:| -One of the biggest challenges organizations face is onboarding new developers to projects. There are libraries to install, services to configure, version issues, obscure error messages... It can literally take days to get everything running before a developer is able to write their first line of code. [GitHub Codespaces][codespaces] is built to streamline this entire process. You can configure a container for development which your developers can access with just a couple of clicks from basically anywhere in the world. The container runs in the cloud, has everything already setup, and ready to go. Instead of days your developers can start writing code in seconds. +One of the biggest challenges organizations face is onboarding new developers to projects. There are libraries to install, services to configure, version issues, obscure error messages... It can literally take days to get everything running before a developer is able to write their first line of code. + +[GitHub Codespaces][codespaces] is built to streamline this entire process. You can configure a container for development which your developers can access with just a couple of clicks - from basically anywhere in the world. The container runs in the cloud, has everything already setup, and ready to go. Instead of days, your developers can start writing code in seconds. GitHub Codespaces allows you to develop using the cloud-based container and Visual Studio Code in your browser window, meaning no local installation is required; you can do development with a tablet and a keyboard! You can also connect your local instance of [Visual Studio Code][vscode-codespaces]. @@ -14,14 +16,16 @@ Let's explore how to create and configure a codespaces for your project, and see GitHub provides a [default container][github-universal-container] for all repositories. This container is based on a Linux image, and contains many popular runtimes including Node.js, Python, PHP and .NET. In many scenarios, this default container might be all you need. You also have the ability to configure a custom container for the repository, as you'll see later in this exercise. For now, let's explore how to use the default container. 1. If not already open, open your repository in your browser. -1. From the **Code** tab (suggest to open a new browser tab) in your repo, access the green **<> Code** dropdown button and from the **Codespaces** tab click **Create codespace on main**. -1. Allow the Codespace to load; it should take less than 30 seconds because we are using the default image. +2. From the **Code** tab (suggest to open a new browser tab) in your repo, access the green **<> Code** dropdown button and from the **Codespaces** tab click **Create codespace on main**. +3. Allow the Codespace to load; it should take less than 30 seconds because we are using the default image. ## Defining a custom container One thing that's really great is the [default dev container][github-universal-container-definition] has **.NET 7**, **node**, **python**, **mvn**, and more. But what if you need other tools? Or in our case, we want don't want to have each developer install the **[GitHub Copilot Extension][copilot-extension]**; we want to have everything pre-configured from the start! -Let's create our own dev container! The [dev container is configured][dev-containers-docs] by creating the Docker files Codespaces will use to create and configure the container, and providing any customizations in the `devcontainer.json` file. Customizations provided in `devcontainer.json` can include ports to open, commands to run, and extension to install in Visual Studio Code (either running locally on the desktop or in the browser). This configuration becomes part of the repository. All developers who wish to contribute can then create a new instance of the container based on the configuration you provided. +Let's create our own dev container! + +The [dev container is configured][dev-containers-docs] by creating the Docker files Codespaces will use to create and configure the container, and providing any customizations in the `devcontainer.json` file. Customizations provided in `devcontainer.json` can include ports to open, commands to run, and extension to install in Visual Studio Code (either running locally on the desktop or in the browser). This configuration becomes part of the repository. All developers who wish to contribute can then create a new instance of the container based on the configuration you provided. 1. Access the Command Palette (F1 or clicking ☰ → View → Command Palette), then start typing **dev container**. 2. Select **Codespaces: Add Development Container Configuration Files...** . @@ -31,6 +35,7 @@ Let's create our own dev container! The [dev container is configured][dev-contai 6. Select the following features to add into your container: - **Azure CLI** - **GitHub CLI** + - **Playwright** - **Python** > [!NOTE] @@ -49,7 +54,7 @@ You have now defined the container to be used by your codespace. This contains t Creating a development environment isn't solely focused on the services. Developers rely on various extensions and plugins for their [integrated development environments (IDEs)][IDE]. To ensure consistency, you may want to define a set of extensions to automatically install. When using GitHub Codespaces and either a local instance of Visual Studio Code or the browser-based version, you can add a list of [extensions][vscode-extensions] to the **devcontainer.json** file. -Before rebuilding the container, let's add **GitHub.copilot** to the list of extensions. +Before rebuilding the container, let's add the project-specific extensions our developers will want to the list defined in **devcontainer.json**. 1. Remaining in the codespace, open **devcontainer.json** inside the **.devcontainer** folder. 2. Locate the following section: @@ -81,7 +86,12 @@ Before rebuilding the container, let's add **GitHub.copilot** to the list of ext }, ``` -5. Just below the customizations, paste the following code to provide the list of ports which should be made available for development by the codespace: +## Add forwarded ports + +As we're working on the website we'll want to be able to run it and confirm things are working as we expected. Codespaces allows you to [forward ports][codespaces-forward-ports], allowing you to access them, almost as if they were running locally. Let's add the ports for our webapp to the list of ones to be forwarded by the codespace. + +1. Return to **devcontainer.json**. +2. Just below the `customizations` section you utilized a moment ago, paste the following code to provide the list of ports which should be made available for development by the codespace: ```json "forwardPorts": [ @@ -91,7 +101,12 @@ Before rebuilding the container, let's add **GitHub.copilot** to the list of ext ], ``` -6. Just below the list of ports, add the command to run the startup script to the container definition: +## Startup scripts + +You frequently will need to run scripts to finish the configuration of an environment, or to start the required services. To support this, you can define a [`postStartCommand`][vscode-post-start-command] in your **devcontainer.json**. In our case, we want to ensure the app starts to streamline debugging. Let's get that added. + +1. Return to **devcontainer.json**. +2. Just below the `forwardedPorts` section you utilized a moment ago, add the command to run the startup script to the container definition: ```json "postStartCommand": "chmod +x /workspaces/dog-shelter/scripts/start-app.sh && /workspaces/dog-shelter/scripts/start-app.sh", @@ -104,8 +119,8 @@ You've now defined a custom container! Whenever someone uses the codespace you defined they'll have an environment with Node.js and Mongo DB, and the GitHub Copilot extension installed. Let's use this container! 1. Access the Command Palette (F1 or clicking ☰ → View → Command Palette), then start typing **dev container**. -1. Type **rebuild** and select **Codespaces: Rebuild container**. -1. Select **Rebuild Container** on the dialog box. Your container now rebuilds. +2. Type **rebuild** and select **Codespaces: Rebuild container**. +3. Select **Rebuild Container** on the dialog box. Your container now rebuilds. > [!IMPORTANT] > Rebuilding the container can take several minutes. Obviously this isn't an ideal situation for providing fast access to your developers, even if it's faster than creating everything from scratch. Fortunately you can [prebuild your codespaces][codespace-prebuild] to ensure developers can spin one up within seconds. @@ -114,7 +129,7 @@ Whenever someone uses the codespace you defined they'll have an environment with ## Interacting with the repository -Custom containers for GitHub Codespaces become part of the source code for the repository. Thus they are maintained through standard source control, and will follow the repository as it's forked in the future. This allows this definition to be shared across all developers contributing to the project. Let's upload our new configuration, closing the [issue you created][walkthrough-previous] for defining a development environment. +Custom containers for GitHub Codespaces become part of the source code for the repository. Thus they are maintained through standard source control, and will follow the repository as it's forked in the future. This allows this definition to be shared across all developers contributing to the project. Let's upload our new configuration, closing the [issue you created][previous] for defining a development environment. > [!IMPORTANT] > For purposes of this exercise we are pushing code updates directly to `main`, our default branch. Normally you would follow the [GitHub flow][github-flow], which we will do in a [later exercise][github-flow-exercise]. @@ -149,29 +164,34 @@ Custom containers for GitHub Codespaces become part of the source code for the r ## Summary and next steps -Congratulations! You have now defined a custom development environment including all services and extensions. This eliminates the initial setup hurdle normally required when contributing to a project. Let's use this codespace to [implement testing and continuous integration][walkthrough-next] for the project. + +Congratulations! You have now defined a custom development environment including all services and extensions. This eliminates the initial setup hurdle normally required when contributing to a project. Let's use this codespace to [implement testing and continuous integration][next] for the project. ## Resources + - [GitHub Codespaces][codespaces] - [Getting started with GitHub Codespaces][codespaces-docs] - [Defining dev containers][dev-containers-docs] - [GitHub Skills: Code with Codespaces][skills-codespaces] -| [← Project management with GitHub Issues][walkthrough-previous] | [Next: Continuous integration and testing →][walkthrough-next] | +| [← Project management with GitHub Issues][previous] | [Next: Continuous integration and testing →][next] | |:-----------------------------------|------------------------------------------:| +[next]: ./4-continuous-integration.md +[previous]: ./2-issues.md +[github-flow-exercise]: ./6-github-flow.md + +[codespace-prebuild]: https://docs.github.com/codespaces/prebuilding-your-codespaces [codespaces]: https://github.com/features/codespaces +[codespaces-docs]: https://docs.github.com/codespaces/overview +[codespaces-forward-ports]: https://docs.github.com/codespaces/developing-in-a-codespace/forwarding-ports-in-your-codespace [copilot-extension]: https://marketplace.visualstudio.com/items?itemName=GitHub.copilot -[codespaces-docs]: https://docs.github.com/en/codespaces/overview -[codespace-prebuild]: https://docs.github.com/en/codespaces/prebuilding-your-codespaces [dev-containers-docs]: https://docs.github.com/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers -[github-flow]: https://docs.github.com/en/get-started/quickstart/github-flow -[github-flow-exercise]: ./7-github-flow.md +[github-flow]: https://docs.github.com/get-started/quickstart/github-flow [github-universal-container]: https://docs.github.com/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers#using-the-default-dev-container-configuration [github-universal-container-definition]: https://github.com/devcontainers/images/blob/main/src/universal/.devcontainer/Dockerfile [IDE]: https://en.wikipedia.org/wiki/Integrated_development_environment [skills-codespaces]: https://github.com/skills/code-with-codespaces -[vscode-codespaces]: https://docs.github.com/en/codespaces/developing-in-codespaces/using-github-codespaces-in-visual-studio-code +[vscode-codespaces]: https://docs.github.com/codespaces/developing-in-codespaces/using-github-codespaces-in-visual-studio-code [vscode-extensions]: https://code.visualstudio.com/docs/editor/extension-marketplace -[walkthrough-previous]: 2-issues.md -[walkthrough-next]: 4-testing.md +[vscode-post-start-command]: https://code.visualstudio.com/remote/advancedcontainers/start-processes diff --git a/content/full-day/4-testing.md b/content/4-continuous-integration.md similarity index 62% rename from content/full-day/4-testing.md rename to content/4-continuous-integration.md index 649f84e..b02c271 100644 --- a/content/full-day/4-testing.md +++ b/content/4-continuous-integration.md @@ -1,6 +1,6 @@ # Continuous integration and testing -| [← Cloud-based development with GitHub Codespaces][walkthrough-previous] | [Next: Helping GitHub Copilot understand context →][walkthrough-next] | +| [← Cloud-based development with GitHub Codespaces][previous] | [Next: Coding with GitHub Copilot →][next] | |:-----------------------------------|------------------------------------------:| Chances are you've heard the abbreviation CI/CD, which stands for continuous integration and continuous delivery (or sometimes continuous deployment). CI is centered on incorporating new code into the existing codebase, and typically includes running tests and performing builds. CD focuses on the next logical step, taking the now validated code and generating the necessary outputs to be pushed to the cloud or other destinations. This is probably the most focused upon component of DevOps. @@ -13,22 +13,17 @@ CI/CD fosters a culture of rapid development, collaboration, and continuous impr A set of unit tests exist for the Python server for the project. You want to ensure those tests are run whenever someone makes a [pull request][about-prs] (PR). To meet this requirement, you'll need to define a workflow for the project, and ensure there is a [trigger][workflow-triggers] for pull requests to main. Fortunately, [GitHub Copilot][copilot] can aid you in creating the necessary YML file! -## Exploring the test +## Exploring the test and project -Let's take a look at the tests defined for the project. - -> [!NOTE] -> There are only a few tests defined for this project. Many projects will have hundreds or thousands of tests to ensure reliability. +Let's take a look at the tests defined for the project, and the supporting script. 1. Return to your codespace, or reopen it by navigating to your repository and selecting **Code** > **Codespaces** and the name of your codespace. 2. In **Explorer**, navigate to **server** and open **test_app.py**. -3. Open GitHub Copilot Chat and ask for an explanation of the file. - -> [!NOTE] -> Consider using the following GitHub Copilot tips to gain an understanding of the tests: -> -> - `/explain` is a [slash command][copilot-slash-commands] to quickly ask for an explanation -> - Highlight specific sections of the file to focus on areas you may have questions about +3. Open GitHub Copilot Chat. +4. Utilizing the dropdowns below the prompt window, ensure **Ask** and **GPT-4.1** are selected for the mode and model respectively. +5. In your own words, ask for an explanation of the tests in the file. +6. In **Explorer**, navigate to **scripts** and open **run-server-tests.sh**. +7. Open GitHub Copilot Chat and, in your own words, ask for an explanation of the script which is used to run the Python tests for our Flask server. ## Understanding workflows @@ -45,55 +40,50 @@ Creating a YML file can be a little tricky. Fortunately, GitHub Copilot can help ## Create the workflow file +> [!NOTE] +> You will notice a workflow already exists to run Playwright tests. These are part of the project to streamline library version updates. For this exercise you can ignore those tests, but we'll refer to them in a later exercise. + Now that we have an overview of the structure of a workflow, let's ask Copilot to generate it for us! -1. Create a new folder under **.github** named **workflows**. -2. Create a new file named **server-test.yml** and ensure the file is open. -3. If prompted to install the **GitHub Actions** extension, select **Install**. -4. Open GitHub Copilot Chat. -5. Add the test file **test_app.py** to the context by using the `#` in the Chat dialog box and beginning to type **test_app.py**, and pressing enter when it's highlighted. -6. Prompt Copilot to create a GitHub Action workflow to run the tests. Use natural language to describe the workflow you're looking to create (to run the tests defined in test_app.py), and that you want it to run on merge (for when new code is pushed), when a PR is made, and on demand. +1. Return to your codespace. +2. Ensure **run-server-tests.sh** is the active editor so Copilot will utilize the file for context when performing the task. +3. Use the following prompt to ask Copilot to create a new GitHub Actions workflow to run the tests, or modify it into your own words: - > [!IMPORTANT] - > A prescriptive prompt isn't provided as part of the exercise is to become comfortable interacting with GitHub Copilot. + Create a new GitHub Actions workflow to run the run-server-tests script for any PR or merge into main. Ensure least privilege is used in the workflow. -6. Add the generated code to the new file by hovering over the suggested code and selecting the **Insert at cursor** button. The generated code should resemble the following: +3. Copilot will explore the project, and generate the necessary YAML for the workflow. It should look like the example below: ```yml name: Server Tests on: push: - branches: [ main ] - paths: - - 'server/**' + branches: [main] pull_request: - branches: [ main ] - paths: - - 'server/**' + branches: [main] + +permissions: + contents: read jobs: - server-test: + server-tests: + name: Run Server Unit Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f server/requirements.txt ]; then pip install -r server/requirements.txt; fi - pip install pytest - - - name: Run tests - working-directory: ./server - run: | - python -m pytest test_app.py -v + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Make scripts executable + run: chmod +x scripts/run-server-tests.sh scripts/setup-environment.sh + + - name: Run server tests + run: ./scripts/run-server-tests.sh ``` > [!IMPORTANT] @@ -110,26 +100,26 @@ With the workflow created, let's push it to the repository. Typically you would > All commands are entered using the terminal window in the codespace. 1. Use the open terminal window in your codespace, or open it (if necessary) by pressing Ctl + `. -1. List all issues for the repository by entering the following command in the terminal window: +2. List all issues for the repository by entering the following command in the terminal window: ```bash gh issue list ``` -1. Note the issue number for the one titled **Implement testing**. -1. Stage all files by entering the following command in the terminal window: +3. Note the issue number for the one titled **Implement testing**. +4. Stage all files by entering the following command in the terminal window: ```bash git add . ``` -1. Commit all changes with a message by entering the following command in the terminal window, replacing **** with the number for the **Implement testing** issue: +5. Commit all changes with a message by entering the following command in the terminal window, replacing **** with the number for the **Implement testing** issue: ```bash git commit -m "Resolves #" ``` -1. Push all changes to the repository by entering the following command in the terminal window: +6. Push all changes to the repository by entering the following command in the terminal window: ```bash git push @@ -151,27 +141,29 @@ You've now seen a workflow, and explore the details of a run! ## Summary and next steps -Congratulations! You've implemented automated testing, a standard part of continuous integration, which is critical to successful DevOps. Automating these processes ensures consistency and reduces the workload required for developers and administrators. You have created a workflow to run tests on any new code for your codebase. Let's explore [context with GitHub Copilot chat][walkthrough-next]. +Congratulations! You've implemented automated testing, a standard part of continuous integration, which is critical to successful DevOps. Automating these processes ensures consistency and reduces the workload required for developers and administrators. You have created a workflow to run tests on any new code for your codebase. Let's explore [coding with GitHub Copilot][next]. ### Resources + - [GitHub Actions][github-actions] - [GitHub Actions Marketplace][actions-marketplace] - [About continuous integration][about-ci] - [GitHub Skills: Test with Actions][skills-test-actions] -| [← Cloud-based development with GitHub Codespaces][walkthrough-previous] | [Next: Helping GitHub Copilot understand context →][walkthrough-next] | +| [← Cloud-based development with GitHub Codespaces][previous] | [Next: Coding with GitHub Copilot →][next] | |:-----------------------------------|------------------------------------------:| -[about-ci]: https://docs.github.com/en/actions/automating-builds-and-tests/about-continuous-integration -[about-prs]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests +[next]: ./5-code.md +[previous]: ./3-codespaces.md +[github-flow-exercise]: ./6-github-flow.md +[issues-exercise]: ./2-issues.md + +[about-ci]: https://docs.github.com/actions/automating-builds-and-tests/about-continuous-integration +[about-prs]: https://docs.github.com/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests [actions-marketplace]: https://github.com/marketplace?type=actions -[workflow-triggers]: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows [copilot]: https://gh.io/copilot -[copilot-slash-commands]: https://docs.github.com/en/copilot/using-github-copilot/copilot-chat/github-copilot-chat-cheat-sheet +[copilot-slash-commands]: https://docs.github.com/copilot/using-github-copilot/copilot-chat/github-copilot-chat-cheat-sheet [github-actions]: https://github.com/features/actions -[github-flow]: https://docs.github.com/en/get-started/quickstart/github-flow -[github-flow-exercise]: ./7-github-flow.md -[issues-exercise]: ./2-issues.md +[github-flow]: https://docs.github.com/get-started/quickstart/github-flow [skills-test-actions]: https://github.com/skills/test-with-actions -[walkthrough-previous]: 3-codespaces.md -[walkthrough-next]: 5-context.md +[workflow-triggers]: https://docs.github.com/actions/reference/events-that-trigger-workflows diff --git a/content/5-code.md b/content/5-code.md new file mode 100644 index 0000000..2ec4263 --- /dev/null +++ b/content/5-code.md @@ -0,0 +1,96 @@ +# Coding with GitHub Copilot + +| [← Continuous integration and testing][previous] | [Next: GitHub flow →][next] | +|:-----------------------------------|------------------------------------------:| + +To truly experience the newly created workflow in action, and eventually the GitHub flow (which we'll see in the next exercise) we're going to add a small feature to our website. You'll do this by asking [GitHub Copilot][github-copilot] to generate the required code - and tests. + +> [!IMPORTANT] +> The focus of this workshop is on DevOps with GitHub. If you'd like to explore more about GitHub Copilot in workshop form, you can see [Agents in SDLC][agents-in-sdlc], which is also available on [GitHub-samples][github-samples]. + +## Scenario + +The website currently lists just the name and breed of the dog on the landing page. Users would like to see at a glance the adoption status so they don't get their hopes up only to discover a dog isn't available. You'll utilize Copilot to add the feature, as well as generate and run the tests to confirm the updates. + +## Overview of this exercise + +To streamline the creation of both the feature and required infrastructure you'll utilize GitHub Copilot agent mode to generate the code and tests. + +## GitHub Copilot agent mode + +In the [prior exercise][previous], you utilized **ask mode** in GitHub Copilot. Ask mode is focused on "single-turn" operations, where you ask a question, receive an answer, and then repeat the flow as needed. Ask mode is great for generating individual files, learning about your project, and generic code-related questions. + +[**Agent mode**][agent-mode] allows Copilot to act like a peer programmer, both generating code suggestions and performing tasks on your behalf. Agent mode will explore your project, build an approach of how to resolve a problem, generate the code, perform supporting operations like running tests, and even self-heal should it find any problems. + +> [!IMPORTANT] +> Before Copilot agent mode runs any external commands it will ask for confirmation. This allows you to ensure it's doing the right thing, cancelling any incorrect operations. + +By using agent mode, we'll be able to both create the code and tests, but have Copilot run the tests and correct any mistakes it might find. + +## Create the filter and tests + +Let's ask Copilot to generate the code to add the feature and tests! + +1. Return to your codespace, or reopen it by navigating to your repository and selecting **Code** > **Codespaces** and the name of your codespace. +2. Open Copilot Chat. +3. Below the prompt dialog, ensure **Agent** is selected from the mode dropdown on the left. +4. Use the following prompt to ask Copilot to generate the necessary code and tests to implement filtering: + + ```markdown + Let's update the site to have an adoption status flag for the dogs! Create the necessary tests and ensure they all pass. + ``` + +> [!IMPORTANT] +> Because AI is probabilistic rather than deterministic, the exact flow, code generated, and files changed will vary. We've highlighted a likely flow you'll experience with Copilot, but the specifics may be a bit different Copilot performs its tasks for you. +> +> The desired outcome is to have all tests passing, and the feature implemented. The exact code or look and feel are secondary to this goal. + +5. Copilot agent mode gets to work on the project. You will notice it begins by exploring the project, determining what's already there, and coming up with a plan. It will then work on generating the necessary code and tests. +6. As Copilot performs its operations, you'll occasionally be prompted by Copilot to execute commands to run the tests and other operations. Review the commands and, as appropriate, select **Continue**. +7. Once Copilot completes its work, select **Keep** in the chat window to keep all files. + +> [!NOTE] +> There's always a chance Copilot may do the wrong thing or be unsuccessful at completing the task. While care was taken when building out the lab and scenario, mistakes can happen. If you get stuck, you can start new chat and request Copilot perform the task again, or consult with the workshop leader. + +## Validating the changes + +With the newly generated code in place, let's take a moment to ensure the site has the new behavior! + +1. Open a terminal window in shell in your codespace by selecting Ctl + Shift + \`. +2. Run the following command to start the site: + + ```shell + ./scripts/start-app.sh + ``` + +3. Once the output indicates the site has started, hold down Cmd (or Ctl on a PC) and click on the URL for `http://localhost:4321`. +4. When page opens, view the updates, noticing the flag added. + +> [!NOTE] +> The exact look and feel may vary depending on the code Copilot generated. + +5. Stop the site by returning to your codespace, clicking on the terminal window, and selecting Ctl + C. + +## Summary and next steps + +Congratulations! You've worked with GitHub Copilot to a new flag to the site. Now it's time to take that feature and kickoff the rest of the DevOps flow. Let's close out by [creating a pull request with our new functionality][next]! + +## Resources + +- [Asking GitHub Copilot questions in your IDE][copilot-questions] +- [Copilot Edits][copilot-chat-edits] +- [Copilot Chat cookbook][copilot-chat-cookbook] + +| [← Continuous integration and testing][previous] | [Next: GitHub flow →][next] | +|:-----------------------------------|------------------------------------------:| + +[next]: ./6-github-flow.md +[previous]: ./4-continuous-integration.md + +[agent-mode]: https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode +[agents-in-sdlc]: https://github.com/github-samples/agents-in-sdlc +[copilot-chat-cookbook]: https://docs.github.com/copilot/copilot-chat-cookbook +[copilot-chat-edits]: https://code.visualstudio.com/docs/copilot/copilot-edits +[copilot-questions]: https://docs.github.com/copilot/using-github-copilot/copilot-chat/asking-github-copilot-questions-in-your-ide +[github-copilot]: https://docs.github.com/copilot/get-started/what-is-github-copilot +[github-samples]: https://github.com/github-samples/ diff --git a/content/full-day/7-github-flow.md b/content/6-github-flow.md similarity index 73% rename from content/full-day/7-github-flow.md rename to content/6-github-flow.md index 455ac95..4bea841 100644 --- a/content/full-day/7-github-flow.md +++ b/content/6-github-flow.md @@ -1,16 +1,16 @@ # GitHub flow -| [← Add new functionality][walkthrough-previous] | [Next: Deploy the application →][walkthrough-next] | +| [← Coding with GitHub Copilot][previous] | [Next: Deploy the application →][next] | |:-----------------------------------|------------------------------------------:| The [GitHub flow][github-flow] is a lightweight, [branch-based][about-branches] workflow. It's designed to allow for free testing and exploration of ideas and novel approaches which are then reviewed and, if accepted, brought into the codebase. At a high level, the GitHub flow follows this pattern: 1. Create a branch -1. Make the desired changes -1. Create a [pull request][about-prs] -1. Review changes, gather feedback and make updates -1. Review results of automated operations such as testing for continuous integration -1. If changes are approved, merge into codebase +2. Make the desired changes +3. Create a [pull request][about-prs] +4. Review changes, gather feedback and make updates +5. Review results of automated operations such as testing for continuous integration +6. If changes are approved, merge into codebase The GitHub flow is designed to work as a cycle, where contributors continuously explore, test, review, and build upon their work and the work of others. @@ -19,7 +19,7 @@ The GitHub flow is designed to work as a cycle, where contributors continuously ## Scenario -With the code changes created in the [prior exercise][code-exercise], it's time to walk through the GitHub flow to create a pull request and incorporate the updates into the codebase. While the changes have already been made (meaning we are slightly out of order from the "traditional" flow), you can still perform the steps to explore. +With the code changes created in the [prior exercise][previous], it's time to walk through the GitHub flow to create a pull request and incorporate the updates into the codebase. While the changes have already been made (meaning we are slightly out of order from the "traditional" flow), you can still perform the steps to explore. ## Creating a branch @@ -28,11 +28,11 @@ A [branch][about-branches] is a copy of the code stored in the same repository. There are different ways to create a branch when using [GitHub Codespaces][github-codespaces]. You can utilize the command-line to run [git](https://git-scm.com/docs/git-branch) commands. You can use the Source Control pane in your codespace to get the support of the UI for creating your branch. In our example we're going to use the command-line to create the branch. 1. Return to your codespace, or reopen it by navigating to your repository and selecting **Code** > **Codespaces** and the name of your codespace. -2. Open a **terminal window** by pressing Ctl + `. -3. In the terminal window, enter the following command to create and switch to a new branch named `add-filter`: +2. Open a **terminal window** by pressing Ctl + \`. +3. In the terminal window, enter the following command to create and switch to a new branch named `add-flag`: ```bash - git checkout -b add-filter + git checkout -b add-flag ``` 4. Stage all code to be committed to the new branch by entering the following command in the terminal window: @@ -49,7 +49,7 @@ There are different ways to create a branch when using [GitHub Codespaces][githu 7. Finally, push the new branch to the repository by entering the following command in the terminal window: ```bash - git push -u origin add-filter + git push -u origin add-flag ``` ## Create the pull request to suggest updates @@ -59,16 +59,16 @@ A [pull request][about-prs] is a request to pull or incorporate new code into th Pull requests can be made through the source control pane in the codespace, the repository's website, or through the command-line using the [GitHub CLI][github-cli]. In our example we're going to create the pull request in the CLI, then navigate to the website to see the pull request and the actions running, and merge the code into the codebase. 1. Return to your codespace. -1. Find the number for the [issue you created earlier][issues-exercise] titled **Add component to display hours** by entering the following command in the terminal window: +2. Find the number for the [issue you created earlier][issues-exercise] titled **Add component to display hours** by entering the following command in the terminal window: ```bash gh issue list ``` -1. Create a pull request with the title **Add hours component** and body **Resolves #\**, replacing **\** with the issue number you obtained in the previous step by entering the following command in the terminal window: +3. Create a pull request with the title **Add adoption status flag** and body **Resolves #\**, replacing **\** with the issue number you obtained in the previous step by entering the following command in the terminal window: ```bash - gh pr create -t "Add hours component" -b "Resolves #" + gh pr create -t "Add adoption status flag" -b "Resolves #" ``` ## Explore and merge the pull request @@ -80,14 +80,14 @@ In our scenario, we created an automated workflow for front-end tests for our ap Let's explore the pull request and watch the workflows run. We'll ensure the tests now run successfully and, assuming they do, merge the pull request. 1. Follow the link displayed in the terminal window by using Ctl - **Click** (or Cmd - **Click** on a Mac). -1. In the page displayed, note the workflow running the [end-to-end tests created earlier][testing-exercise] and [code scanning][security-exercise]. -1. When the workflows complete successfully, select **Merge pull request** to merge your changes into the **main** branch. +2. In the page displayed, note the workflow running the [end-to-end tests created earlier][testing-exercise] and [code scanning][security-exercise]. +3. When the workflows complete successfully, select **Merge pull request** to merge your changes into the **main** branch. Congratulations! You've now used the GitHub flow to suggest changes, perform a review, and merge those into your codebase. ## Summary and next steps -The GitHub flow is a workflow for managing changes and incorporating new features into a codebase. GitHub flow gives you the freedom to explore and experiment, while ensuring all code follows a validation process before being merged. Let's get our [application deployed][walkthrough-next]. +The GitHub flow is a workflow for managing changes and incorporating new features into a codebase. GitHub flow gives you the freedom to explore and experiment, while ensuring all code follows a validation process before being merged. Let's get our [application deployed][next]. ## Resources @@ -95,20 +95,20 @@ The GitHub flow is a workflow for managing changes and incorporating new feature - [GitHub Skills: Review pull requests][skills-review-prs] - [GitHub Skills: Release based workflow][skills-release-workflow] -| [← Add new functionality][walkthrough-previous] | [Next: Deploy the application →][walkthrough-next] | +| [← Coding with GitHub Copilot][previous] | [Next: Deploy the application →][next] | |:-----------------------------------|------------------------------------------:| -[about-branches]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches -[about-prs]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests -[cicd-resources]: https://resources.github.com/ci-cd/ -[code-exercise]: ./6-code.md -[github-codespaces]: https://github.com/features/codespaces -[github-cli]: https://cli.github.com/ -[github-flow]: https://docs.github.com/en/get-started/quickstart/github-flow +[next]: ./7-deployment.md +[previous]: ./5-code.md [issues-exercise]: ./2-issues.md [security-exercise]: ./1-code-scanning.md -[skills-review-prs]: https://github.com/skills/review-pull-requests +[testing-exercise]: ./4-continuous-integration.md + +[about-branches]: https://docs.github.com/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches +[about-prs]: https://docs.github.com/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests +[cicd-resources]: https://resources.github.com/ci-cd/ +[github-cli]: https://cli.github.com/ +[github-codespaces]: https://github.com/features/codespaces +[github-flow]: https://docs.github.com/get-started/quickstart/github-flow [skills-release-workflow]: https://github.com/skills/release-based-workflow -[testing-exercise]: ./4-testing.md -[walkthrough-previous]: 6-code.md -[walkthrough-next]: 8-deployment.md +[skills-review-prs]: https://github.com/skills/review-pull-requests diff --git a/content/full-day/8-deployment.md b/content/7-deployment.md similarity index 91% rename from content/full-day/8-deployment.md rename to content/7-deployment.md index e2ea9db..f35d181 100644 --- a/content/full-day/8-deployment.md +++ b/content/7-deployment.md @@ -1,7 +1,7 @@ # Deploying the project to the cloud -| [← GitHub flow][walkthrough-previous] | [Next: Pets workshop selection →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| +| [← GitHub flow][previous] | +|:-----------------------------------| The CD portion of CI/CD is continuous delivery or continuous deployment. In a nutshell, it's about taking the product you're building and putting it somewhere to be accessed by the people who need it. There's numerous ways to do this, and the process can become rather involved. We're going to focus on taking our application and deploying it to Azure. @@ -197,14 +197,13 @@ Work with the workshop leaders as needed to ask questions and get guidance as yo - [Deploying with GitHub Actions][actions-deploy] - [What is the Azure Developer CLI?][azd-docs] -| [← GitHub flow][walkthrough-previous] | [Next: Pets workshop selection →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| +| [← GitHub flow][previous] | +|:-----------------------------------| -[actions-deploy]: https://docs.github.com/en/actions/use-cases-and-examples/deploying/deploying-with-github-actions -[azd-docs]: https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview?tabs=linux +[previous]: ./6-github-flow.md + +[actions-deploy]: https://docs.github.com/actions/use-cases-and-examples/deploying/deploying-with-github-actions +[azd-docs]: https://learn.microsoft.com/azure/developer/azure-developer-cli/overview?tabs=linux [azure-copilot-extension]: https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azure-github-copilot -[bicep-docs]: https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview?tabs=bicep -[extensions-copilot-chat]: ./5-context.md -[oidc-docs]: https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect -[walkthrough-previous]: 7-github-flow.md -[walkthrough-next]: ../README.md \ No newline at end of file +[bicep-docs]: https://learn.microsoft.com/azure/azure-resource-manager/bicep/overview?tabs=bicep +[oidc-docs]: https://docs.github.com/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect \ No newline at end of file diff --git a/content/GitHub-Copilot-Resources.md b/content/GitHub-Copilot-Resources.md deleted file mode 100644 index f1478cb..0000000 --- a/content/GitHub-Copilot-Resources.md +++ /dev/null @@ -1,47 +0,0 @@ -# GitHub Copilot Resources - -Checkout the resources below to dive in and learn more about [GitHub Copilot](https://gh.io/copilot). - -## Getting started - -New to GitHub Copilot? Start here! - -- [GitHub Copilot - Your AI pair programmer](https://github.com/features/copilot) - See all that GitHub Copilot can do. This feature summary highlights all that you can do with GitHub Copilot. See a comparison of what is available in each pricing plan. -- [How AI can make you an awesome developer](https://github.com/orgs/community/discussions/153056) - Staying relevant in this era of AI requires not only adapting to new technologies, but also honing in on your skills. It is extremely relevant to address the elephant in the room, how AI is not going to replace us, but make us much better developers. Let’s explore five key strategies to help you stay relevant and thrive in this new era of AI-driven development. -- [Essential GitHub Copilot resources for enterprise teams](https://resources.github.com/enterprise/essential-copilot-resources/) - GitHub Resources - We've gathered everything enterprise teams need to hit the ground running with GitHub Copilot. From initial setup to advanced features, this guide will walk you through the essential resources to make your Copilot implementation successful. - -## Documentation - -[GitHub Copilot Documentation](https://docs.github.com/en/copilot) contains a robust collection of articles to help you get the most out of the tool. Some key articles to start with include: - -- [Prompt engineering for GitHub Copilot](https://docs.github.com/en/copilot/using-github-copilot/prompt-engineering-for-github-copilot) - A prompt is a request that you make to GitHub Copilot. For example, a question that you ask Copilot Chat, or a code snippet that you ask Copilot to complete. In addition to your prompt, Copilot uses additional context, like the code in your current file and the chat history, to generate a response. Follow the tips in this article to write prompts that generate better responses from Copilot. -- [Asking GitHub Copilot questions in GitHub.com](https://docs.github.com/en/enterprise-cloud@latest/copilot/using-github-copilot/asking-github-copilot-questions-in-githubcom#asking-exploratory-questions-about-a-repository) – See how you can use GitHub Copilot Chat in GitHub.com to answer general questions about software development, or specific questions about the code, issues, security alerts, pull requests, etc. in a repository. For example: open a specific file and ask Copilot, “How could I improve this code?”. Trying to understand a new codebase? Copilot can help with that. You can ask Copilot questions to help quickly understand the structure and key components of repositories. For example, “What does the code in this repo do? What is the tech stack?. -- [Copilot Chat Cookbook](https://docs.github.com/en/copilot/example-prompts-for-github-copilot-chat) - Find examples of prompts to use with GitHub Copilot Chat. -- [Changing the AI model for Copilot Chat](https://docs.github.com/en/enterprise-cloud@latest/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat) & [Changing the AI model for Copilot code completions](https://docs.github.com/en/enterprise-cloud@latest/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-code-completion) - You are not limited to using the default models for Copilot chat and code completions. You can choose from a selection of other models, each with its own particular strengths. You may have a favorite model that you like to use, or you might prefer to use a particular model for inquiring about a specific subject. Here are some notable recent updates: - -## Copilot in VS Code - -As you're exploring using VS Code in this workshop, here are some articles particular to using [GitHub Copilot in VS Code](https://code.visualstudio.com/docs/copilot/overview): - -- [Context for Code Completion](https://code.visualstudio.com/docs/copilot/ai-powered-suggestions#_context) - Get more out of GitHub Copilot by understanding how it uses context from multiple locations in VS Code to provide more relevant suggestions. -- [Making Copilot Chat an expert in your workspace](https://code.visualstudio.com/docs/copilot/workspace-context) - Referencing @workspace in Copilot Chat lets you ask questions about your entire codebase. Based on the question, Copilot intelligently retrieves relevant files and symbols, which it then references in its answer as links and code examples. Grounded in @workspace references, Copilot Chat becomes a domain expert for tasks like: - - Finding existing code in your codebase - - Making plans for complex code edits - - Explaining higher-level concepts in a codebase -- [Best Practices / Prompt Crafting](https://code.visualstudio.com/docs/copilot/prompt-crafting) - This article covers best practices for using GitHub Copilot in Visual Studio Code by using prompt crafting and providing the right context to GitHub Copilot. - -## Videos - -The [GitHub YouTube channel](https://www.youtube.com/@GitHub/videos) hosts many videos highlighting the latest features: - -- [GitHub Copilot Playlist](http://gh.io/GitHub-Copilot-on-YouTube) for **GitHub Copilot** demos and informational videos. -- [GitHub for Beginners](https://www.youtube.com/playlist?list=PL0lo9MOBetEFcp4SCWinBdpml9B2U25-f) - Season 2 of **GitHub for Beginners** is focused on **GitHub Copilot**. - -## Other resources - -Continue your journey: - -- [Essentials of GitHub Copilot - GitHub Resources](https://resources.github.com/learn/pathways/copilot/essentials/essentials-of-github-copilot/) - In this learning pathway module, we’ll cover the most common questions about GitHub Copilot, and we’ll hear from engineering leaders at the top organizations about how they use GitHub Copilot to accelerate the pace of software development and deliver more value to their customers. This has resources for developers and leaders. -- [GitHub Copilot product updates](https://github.blog/changelog/label/copilot) - We are continually adding capabilities and improving GitHub Copilot. Check out the **GitHub Changelog** to stay up to date on everything we ship. -- [The GitHub Blog](https://github.blog/tag/github-copilot) Be sure to check out the most recent GitHub Copilot related blog posts. -- [GitHub Copilot Discussions](https://github.com/orgs/community/discussions/categories/copilot) - Share your feedback, feature suggestions, etc. via **GitHub public feedback discussions** and influence what we’re building. diff --git a/content/README.md b/content/README.md index c3fe1aa..f025e48 100644 --- a/content/README.md +++ b/content/README.md @@ -1,12 +1,63 @@ -# Pets workshop +# Modern DevOps with GitHub -This repository contains two workshops: +| [Next: Workshop setup →][next] | +|:-----------------------------------:| -- a [one hour](./1-hour/README.md) workshop focused on GitHub Copilot. -- a [full-day](./full-day/README.md) workshop which covers a full day-in-the-life of a developer using GitHub for their DevOps processes. +[DevOps][devops] is a [portmanteau][portmanteau] of **development** and **operations**. At its core is a desire to bring development practices more inline with operations, and operations practices more inline with development. This fosters better communication and collaboration between teams, breaks down barriers, and gives everyone an investment in ensuring customers are delighted by the software we ship. -Both workshops are built around a fictional dog shelter, where you are a volunteer helping them build out their website. +This workshop is built to help guide you through some of the most common DevOps tasks on GitHub. You will: -## Get started +- manage a project with [GitHub Issues][github-issues]. +- create a development environment with [GitHub Codespaces][github-codespaces]. +- use [GitHub Copilot][github-copilot] as your AI pair programmer. +- secure the development pipeline with [GitHub Advanced Security][github-security]. +- automate tasks and CI/CD with [GitHub Actions][github-actions]. -To get started, you choose the option above based on the event you're attending, or as indicated by your workshop mentor. +## Prerequisites + +The application for the workshop uses is built primarily with Python (Flask and SQLAlchemy) and Astro (using Tailwind and Svelte). Experience with these frameworks and languages is not required for the course, as the primary focus will be around GitHub features. + +## Required resources + +To complete this workshop, you will need the following: + +- A [GitHub account][github-signup] +- Access to [GitHub Copilot][github-copilot] + +> [!IMPORTANT] +> This workshop is designed to utilize the free version of GitHub Copilot, the [free compute for Codespaces][codespaces-free], and the functionality provided by GHAS and Actions to public repos. + +## Getting started + +Ready to get started? Let's go! The workshop scenario imagines you as a developer volunteering your time for a pet adoption center. You will work through the process of creating a development environment, creating code, enabling security, and automating processes. + +0. [Setup your environment][next] for the workshop +1. [Enable Code Scanning][code-scanning] to ensure new code is secure +2. [Create an issue][issues] to document a feature request +3. [Create a codespace][codespaces] to start writing code +4. [Implement continuous integration][testing] to supplement development +5. [Add features to your app][code] with GitHub Copilot +6. [Use the GitHub flow][github-flow] to incorporate changes into your codebase +7. [Deploy your application][deployment] to Azure to make your application available to users + +| [Next: Workshop setup →][next] | +|:------------------------------------------:| + +[next]: ./0-setup.md +[code]: ./5-code.md +[code-scanning]: ./1-code-scanning.md +[codespaces]: ./3-codespaces.md +[deployment]: ./7-deployment.md +[github-flow]: ./6-github-flow.md +[issues]: ./2-issues.md +[testing]: ./4-continuous-integration.md + +[codespaces-free]: https://docs.github.com/billing/concepts/product-billing/github-codespaces#free-and-billed-use-by-personal-accounts +[devops]: https://en.wikipedia.org/wiki/DevOps +[github-actions]: https://github.com/features/actions +[github-codespaces]: https://github.com/features/codespaces +[github-copilot]: https://github.com/features/copilot +[github-issues]: https://github.com/features/issues +[github-security]: https://github.com/features/security +[github-signup]: https://github.com/join +[portmanteau]: https://www.merriam-webster.com/dictionary/portmanteau diff --git a/content/full-day/5-context.md b/content/full-day/5-context.md deleted file mode 100644 index 9c10edc..0000000 --- a/content/full-day/5-context.md +++ /dev/null @@ -1,130 +0,0 @@ -# Helping GitHub Copilot understand context - -| [← Implement testing][walkthrough-previous] | [Next: Coding with GitHub Copilot →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -The key to success when coding (and much of life) is context. Before we add code to a codebase, we want to understand the rules and structures already in place. When working with an AI coding assistant such as GitHub Copilot the same concept applies - the quality of suggestion is directly proportional to the context Copilot has. Let's use this opportunity to both explore the project we've been given and how to interact with Copilot to ensure it has the context it needs to do its best work. - -## Scenario - -Before adding new functionality to the website, you want to explore the existing structure to determine where the updates need to be made. You also want to provide Copilot some context in the form of [custom instructions][copilot-custom-instructions] so it has a better idea of how best to generate code. - -## Getting started with GitHub Copilot - -GitHub Copilot is a cloud-based service offered for both individuals and businesses. As an individual, you can [sign up for a free account][copilot-signup] of the service. After enrolling you will typically install the extension for your IDE, which is available for [Visual Studio][copilot-vs], [Visual Studio Code][copilot-vscode], [NeoVIM][copilot-vim], the [JetBrains IDEs][copilot-jetbrains], [XCode](copilot-xcode) and [Eclipse][copilot-eclipse]. Because we'll be using the [Codespace][walkthrough-codespaces] you defined in the previous exercise, you won't need to manually install the extension - you did that when you configured the dev container! - -1. If you don't already have access to GitHub Copilot, [sign up for a free trial][copilot-signup]. -2. In the [previous exercise][walkthrough-codespaces] you configured your [devcontainer][devcontainer-docs] to automatically install the extension for GitHub Copilot, so you're all set and ready to go! - -## Chat participants and extensions - -GitHub Copilot Chat has a set of available chat participants and extensions available to you to both provide instructions to GitHub Copilot and access external services. Chat participants are helpers which work inside your IDE and have access to your project, while extensions can call external services and provide information to you without having to open separate tools. We're going to focus on one core chat participant - `@workspace`. - -`@workspace` creates an index of your project and allows you to ask questions about what you're currently working on, to find resources inside the project, or add it to the context. It's best to use this when the entirety of your project should be considered or you're not entirely sure where you should start looking. In our current scenario, since we want to ask questions about our project, `@workspace` is the perfect tool for the job. - -> [!NOTE] -> This exercise doesn't provide specific prompts to type, as part of the learning experience is to discover how to interact with Copilot. Feel free to talk in natural language, describing what you're looking for or need to accomplish. - -1. Return to your codespace, or reopen it by navigating to your repository and selecting **Code** > **Codespaces** and the name of your codespace. -2. Open GitHub Copilot Chat. -3. Select the `+` icon towards the top to begin a new chat. -4. Type `@workspace` in the chat prompt window and hit tab to select or activate it, then continue by asking Copilot about your project. You can ask what technologies are in use, what the project does, where functionality resides, etc. -5. Spend a few minutes exploring to find the answers to the following questions: - - What frameworks are currently in use? - - Where's the database the project uses? - - How is the frontend built? - - How is the backend built? - - What files are involved in listing dogs? - -## Providing custom instructions - -Context is key to ensuring the code suggestions you receive from GitHub Copilot align with your expectations. When operating with limited information, Copilot makes assumptions about what you're looking for, and can sometimes guess incorrectly. By providing context, you allow Copilot to better align with your objectives. One great way to do this is by building a [copilot-instructions.md][copilot-custom-instructions] file. This markdown file is placed in your **.github** folder and becomes part of your project. You can use this file to indicate various coding standards you wish to follow, the technologies your project uses, or anything else important for Copilot Chat to understand when generating suggestions. - -> [!IMPORTANT] -> The *copilot-instructions.md* file is included in **every** call to GitHub Copilot Chat, and will be part of the context sent to Copilot. Because there is always a limited set of tokens an LLM can operate on, a large set of Copilot instructions can obscure relevant information. As such, you should limit your Copilot instructions file to project-wide information, providing an overview of what you're building and how you're building it. If you need to provide more specific information for particular tasks, you can create [prompt files][copilot-prompt-files] as needed. - -Here are some guidelines to consider when creating a Copilot instructions file: - -- The Copilot instructions file becomes part of the project, meaning it will apply to every developer; anything indicated in the file should be globally applicable. -- The file is markdown, so you can take advantage of that fact by grouping content together to improve readability. -- Provide overview of **what** you are building and **how** you are building it, including: - - languages, frameworks and libraries in use. - - required assets to be generated (such as unit tests) and where they should be placed. - - any language specific rules such as: - - Python code should always follow PEP8 rules. - - use arrow functions rather than the `function` keyword. -- If you notice GitHub Copilot consistently provides an unexpected suggestion (e.g. using class components for React), add those notes to the instructions file. - -Let's create a Copilot instructions file. Just as before, because we want you to explore and experiment, we won't provide exact directions on what to type, but will give enough context to create one on your own. - -1. Create a new file in the **.github** folder called **copilot-instructions.md**. -2. Add the markdown to the file necessary to provide information about the project structure and requirements, including: - - an overview of the project itself (based on the information you gathered earlier in this exercise). - - the languages and frameworks in use to create both the server and client. - - unit tests are required for routes in the Flask app, and must mock the database calls. - - the website should be in dark mode and have a modern look and feel. -3. Save the file! - -Your Copilot instructions file could resemble the following (but again - use your own words and style!): - -```markdown -# Dog shelter - -This is an application to allow people to look for dogs to adopt. It is built in a monorepo, with a Flask-based backend and Astro-based frontend. - -## Backend - -- Built using Flask and SQLAlchemy -- All routes require unit tests, which are created in *test_file.py* in the same folder as the file -- When creating tests, always mock database calls - -## Frontend - -- Built using Astro and Svelte -- Pages should be in dark mode with a modern look and feel -``` - -## Watch the instructions file in action - -Whenever you make a call to Copilot chat, the response will always include the context being used. The context can automatically include the open file (focused on any code you highlight), and individual files or folders you add by using `#file` or `#folder`. You can also include the an index of your workspace by using `@workspace`, as highlighted earlier. The references dialog is a great way to check what information Copilot was using when generating its suggestions and response. Once you create a Copilot instructions file, you will see it's always included in the references section. - -1. Close all files currently open in VS Code or your Codespace. -2. Select the `+` icon in GitHub Copilot chat to start a new chat. -3. Ask Copilot chat **What are the guidelines for the flask app?** -4. Note the references now includes the instructions file and provides information gathered from it. - -![Screenshot of the chat window with the references section expanded displaying Copilot instructions in the list](./images/5-copilot-chat-references.png) - -## Summary and next steps - -Congratulations! You've explored context in GitHub Copilot, which is key to generating quality suggestions. You saw how you can use chat participants to help guide GitHub Copilot, and create a Copilot instructions file to provide an overview of what you're building and how you're building it. With this in place, it's time to turn our attention to [adding new functionality to our website][walkthrough-next]! - -## Resources - -- [Getting started with GitHub Copilot][copilot-getting-started] -- [Adding repository custom instructions for GitHub Copilot][copilot-custom-instructions] -- [Adding personal custom instructions for GitHub Copilot][copilot-personal-instructions] -- [Copilot Chat cookbook][copilot-chat-cookbook] -- [Use Copilot Chat in VS Code][vscode-copilot-chat] - -| [← Implement testing][walkthrough-previous] | [Next: Coding with GitHub Copilot →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[copilot-chat-cookbook]: https://docs.github.com/en/copilot/copilot-chat-cookbook -[copilot-custom-instructions]: https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot -[copilot-eclipse]: https://marketplace.eclipse.org/content/github-copilot -[copilot-getting-started]: https://docs.github.com/en/copilot/getting-started-with-github-copilot -[copilot-jetbrains]: https://plugins.jetbrains.com/plugin/17718-github-copilot -[copilot-prompt-files]: https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot?tool=vscode#about-prompt-files -[copilot-personal-instructions]: https://docs.github.com/en/copilot/customizing-copilot/adding-personal-custom-instructions-for-github-copilot -[copilot-signup]: https://github.com/github-copilot/signup -[copilot-vim]: https://github.com/github/copilot.vim#getting-startedins.com/plugin/17718-github-copilot -[copilot-vs]: https://marketplace.visualstudio.com/items?itemName=GitHub.copilotvs -[copilot-vscode]: https://marketplace.visualstudio.com/items?itemName=GitHub.copilot -[copilot-xcode]: https://github.com/github/CopilotForXcode -[devcontainer-docs]: https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containersopilot/adding-personal-custom-instructions-for-github-copilot -[vscode-copilot-chat]: https://code.visualstudio.com/docs/copilot/copilot-chat -[walkthrough-codespaces]: ./3-codespaces.mdvisualstudio.com/docs/copilot/copilot-chat -[walkthrough-next]: 6-code.md -[walkthrough-previous]: 4-testing.md - diff --git a/content/full-day/6-code.md b/content/full-day/6-code.md deleted file mode 100644 index eb6d5bc..0000000 --- a/content/full-day/6-code.md +++ /dev/null @@ -1,118 +0,0 @@ -# Coding with GitHub Copilot - -| [← Helping GitHub Copilot understand context][walkthrough-previous] | [Next: GitHub flow →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -We've explored how we can use GitHub Copilot to explore our project and to provide context to ensure the suggestions we receive are to the quality we expect. Now let's turn our attention to putting all this prep work into action by generating new code! We'll use GitHub Copilot to aid us in adding functionality to our website and generate the necessary unit tests. - -## Scenario - -The website currently lists all dogs in the database. While this was appropriate when the shelter only had a few dogs, as time has gone on the number has grown and it's difficult for people to sift through who's available to adopt. The shelter has asked you to add filters to the website to allow a user to select a breed of dog and only display dogs which are available for adoption. - -## Overview of this exercise - -In the next handful of steps, you will: - -- create a new Flask endpoint to list the breeds available. -- add the associated unit test. -- update the backend and frontend to display the list and add the filters as required in the scenario. - -## GitHub Copilot interfaces - -Until now, we've primarily focused on GitHub Copilot chat. This will likely be the most common way you'll interact with GitHub Copilot. It allows you to interactively ask questions, and has an ability to perform operations across an individual and (with Copilot Edits) multiple files. You can also get support from GitHub Copilot with code completion, which provides suggestions as you code. We're going to explore each of these three capabilities. - -## Create a new Flask route with Code completion - -Code completion predicts the next block of code you're about to type based on the context Copilot has. For code completion, this includes the file you're currently working on and any tabs open in your IDE. - -> [!IMPORTANT] -> At this time, the Copilot instructions file is only available to Copilot chat. - -Code completion is best for situations where you know what you want to do, and are more than happy to just start writing code with a bit of a helping hand along the way. Suggestions will be generated based both on the code you write (say a function definition) and comments you add to your code. - -> [!NOTE] -> One great way to provide context for GitHub Copilot is to add comments to your code. While comments describing what is done can sometimes be superfluous, it helps Copilot get a better idea of what you're building. - -Let's build our new route in our Flask backend with the help of code completion. - -1. Return to your codespace, or reopen it by navigating to your repository and selecting **Code** > **Codespaces** and the name of your codespace. -2. Open **server/app.py**. -3. Locate the section of code at the very bottom which launches the server, and put your cursor just above it. This should be line 70, and the code will be: - - ```python - if __name__ == '__main__': - app.run(debug=True, port=5100) # Port 5100 to avoid macOS conflicts - ``` - -4. Create the route which will call the database to find all breeds, and returns a JSON array with their names and IDs. If you begin typing `@app.route` or add a comment with the requirements like `# Route to get all breeds`, you should notice italicized text generated by GitHub Copilot. -5. Select Tab to accept the code suggestion. -6. Navigate to [http://localhost:5100/api/breeds][localhost-breeds] to validate the route. - -> [!NOTE] -> As with the prior exercise, we don't provide specific prompts to use with Copilot, as part of the learning experience is to discover how to interact with Copilot. If you are unfamiliar with Flask or how to add routes, you can look at the routes defined above for inspiration, or ask Copilot chat for guidance! - -## Generate the unit tests - -With the route created, we want to now add the tests to ensure the code is correct. We can use GitHub Copilot chat's slash command **/tests** to create the test for us! - -1. Return to your Codespace or VS Code. -2. Highlight the code you generated in the prior step. -3. Open GitHub Copilot chat. -4. Select the `+` button to start a new chat. -5. Type **/tests** and select tab to activate the command, then press enter to run the command. GitHub Copilot will generate the tests! -6. Select the **Apply edits** button just above the generated code suggestion to apply the changes to **test_app.py**. -7. Review and validate the code, making any necessary changes. Select **Keep** once you're satisfied. -> [!IMPORTANT] -> GitHub Copilot, like any generative AI solution, can make mistakes. Always review the generated code, making any necessary changes to ensure it's accurate and performs as expected. -8. Open a terminal window in your codespace or VS Code by selecting Ctl+Shift+` -9. Ensure the virtual server is activated by running the terminal command `source ./venv/bin/activate` -10. Navigate to the **server** folder by running the terminal command `cd server` -11. Run the tests by running the terminal command `python -m unittest` -12. Ensure all tests pass! - -## Add the filters - -Adding the filters to the page will require updating a minimum of three files - the Flask backend, the unit tests for our Flask backend, and the Svelte frontend. Fortunately, Copilot Edits can update multiple files! Let's get our page updated with the help of Copilot Edits. - -1. Open the following files in your IDE (which we'll point Copilot chat to for context): - - **server/app.py** - - **server/test_app.py** - - **client/src/components/DogList.svelte** -2. Open GitHub Copilot Chat. -3. Switch to edit mode by selecting **Edit** in the chat mode dropdown at the bottom of Chat view (should be currently **Ask**) -4. If available, select **Claude 3.7 Sonnet** for the model. -5. Select **Add Context...** in the chat window. -6. Select **server/app.py**, **client/src/components/DogList.svelte** and **server/test_app.py** files (you need to select **Add context** for each file) -> [!TIP] -> If you type the file names after clicking **Add context**, they will show up in the filter. You can also drag the files or right click file in explorer and select `Copilot -> Add File to Chat`) -7. Ask Copilot to perform the operation you want, to update the page to add the filters. It should meet the following requirements: - - A dropdown list should be provided with all breeds - - A checkbox should be available to only show available dogs - - The page should automatically refresh whenever a change is made - - Tests should be updated for any changes to the endpoint. -8. Review the code suggestions to ensure they behave the way you expect them to, making any necessary changes. Once you're satisfied, you can select **Keep** on the files individually or in Copilot Chat to accept all changes. -9. Open the page at [http://localhost:4321][localhost] to see the updates! -10. Run the Python tests by using `python -m unittest` in the terminal as you did previously. -11. If any changes are needed, explain the required updates to GitHub Copilot and allow it to generate the new code. - -> [!IMPORTANT] -> Working iteratively a normal aspect of coding with an AI pair programmer. You can always provide more context to ensure Copilot understands, make additional requests, or rephrase your original prompts. - -## Summary and next steps -Congratulations! You've worked with GitHub Copilot to add new features to the website - the ability to filter the list of dogs. Let's close out by [creating a pull request with our new functionality][walkthrough-next]! - -## Resources -- [Asking GitHub Copilot questions in your IDE][copilot-questions] -- [Copilot Edits][copilot-chat-edits] -- [Copilot Chat cookbook][copilot-chat-cookbook] - -| [← Helping GitHub Copilot understand context][walkthrough-previous] | [Next: GitHub flow →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[copilot-chat-cookbook]: https://docs.github.com/en/copilot/copilot-chat-cookbook -[copilot-chat-edits]: https://code.visualstudio.com/docs/copilot/copilot-edits -[copilot-questions]: https://docs.github.com/en/copilot/using-github-copilot/copilot-chat/asking-github-copilot-questions-in-your-ide -[localhost]: http://localhost:4321 -[localhost-breeds]: http://localhost:5100/api/breeds -[walkthrough-previous]: 5-context.md -[walkthrough-next]: 7-github-flow.md diff --git a/content/full-day/README.md b/content/full-day/README.md deleted file mode 100644 index 111729e..0000000 --- a/content/full-day/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Modern DevOps with GitHub - -| [← Pets workshop selection][walkthrough-previous] | [Next: Workshop setup →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[DevOps][devops] is a [portmanteau][portmanteau] of **development** and **operations**. At its core is a desire to bring development practices more inline with operations, and operations practices more inline with development. This fosters better communication and collaboration between teams, breaks down barriers, and gives everyone an investment in ensuring customers are delighted by the software we ship. - -This workshop is built to help guide you through some of the most common DevOps tasks on GitHub. You'll explore: - -- Managing projects with [GitHub Issues][github-issues] -- Creating a development environment with [GitHub Codespaces][github-codespaces] -- Using [GitHub Copilot][github-copilot] as your AI pair programmer -- Securing the development pipeline with [GitHub Advanced Security][github-security] -- Automating tasks and CI/CD with [GitHub Actions][github-actions] - -## Prerequisites - -The application for the workshop uses is built primarily with Python (Flask and SQLAlchemy) and Astro (using Tailwind and Svelte). While experience with these frameworks and languages is helpful, you'll be using Copilot to help you understand the project and generate the code. As a result, as long as you are familiar with programming you'll be able to complete the exercises! - -## Required resources - -To complete this workshop, you will need the following: - -- A [GitHub account][github-signup] -- Access to [GitHub Copilot][github-copilot] - -## Getting started - -Ready to get started? Let's go! The workshop scenario imagines you as a developer volunteering your time for a pet adoption center. You will work through the process of creating a development environment, creating code, enabling security, and automating processes. - -0. [Setup your environment][walkthrough-next] for the workshop -1. [Enable Code Scanning][code-scanning] to ensure new code is secure -2. [Create an issue][issues] to document a feature request -3. [Create a codespace][codespaces] to start writing code -4. [Implement testing][testing] to supplement continuous integration -5. [Provide Copilot context][context] to generate quality code suggestions -6. [Add features to your app][code] with GitHub Copilot -7. [Use the GitHub flow][github-flow] to incorporate changes into your codebase -8. [Deploy your application][deployment] to Azure to make your application available to users - -## Check out these resources to dive in and learn more -Check out the resources in [**GitHub-Copilot-Resources.md**][GitHub-Copilot-Resources]. - -This resource list has been carefully curated to help you to learn more about GitHub Copilot, how to use it effectively, what is coming in the future and more. There are even YouTube playlists that include the latest videos from the GitHub Developer Relations team and others from GitHub. - -| [← Pets workshop selection][walkthrough-previous] | [Next: Workshop setup →][walkthrough-next] | -|:-----------------------------------|------------------------------------------:| - -[code]: ./6-code.md -[code-scanning]: ./1-code-scanning.md -[codespaces]: ./3-codespaces.md -[context]: ./5-context.md -[deployment]: ./8-deployment.md -[devops]: https://en.wikipedia.org/wiki/DevOps -[github-actions]: https://github.com/features/actions -[github-codespaces]: https://github.com/features/codespaces -[github-copilot]: https://github.com/features/copilot -[github-flow]: ./7-github-flow.md -[github-issues]: https://github.com/features/issues -[github-security]: https://github.com/features/security -[github-signup]: https://github.com/join -[issues]: ./2-issues.md -[portmanteau]: https://www.merriam-webster.com/dictionary/portmanteau -[testing]: ./4-testing.md -[walkthrough-next]: ./0-setup.md -[walkthrough-previous]: ../README.md -[GitHub-Copilot-Resources]: ../GitHub-Copilot-Resources.md diff --git a/content/images/0-setup-configure.png b/content/images/0-setup-configure.png new file mode 100644 index 0000000..95e5850 Binary files /dev/null and b/content/images/0-setup-configure.png differ diff --git a/content/images/0-setup-template.png b/content/images/0-setup-template.png new file mode 100644 index 0000000..dac8959 Binary files /dev/null and b/content/images/0-setup-template.png differ diff --git a/content/full-day/images/1-code-scanning-dialog.png b/content/images/1-code-scanning-dialog.png similarity index 100% rename from content/full-day/images/1-code-scanning-dialog.png rename to content/images/1-code-scanning-dialog.png diff --git a/content/full-day/images/1-code-scanning.png b/content/images/1-code-scanning.png similarity index 100% rename from content/full-day/images/1-code-scanning.png rename to content/images/1-code-scanning.png diff --git a/content/full-day/images/1-dependabot.png b/content/images/1-dependabot.png similarity index 100% rename from content/full-day/images/1-dependabot.png rename to content/images/1-dependabot.png diff --git a/content/full-day/images/1-secret-scanning.png b/content/images/1-secret-scanning.png similarity index 100% rename from content/full-day/images/1-secret-scanning.png rename to content/images/1-secret-scanning.png diff --git a/content/full-day/images/3-open-browser.png b/content/images/3-open-browser.png similarity index 100% rename from content/full-day/images/3-open-browser.png rename to content/images/3-open-browser.png diff --git a/content/full-day/images/3-reload.png b/content/images/3-reload.png similarity index 100% rename from content/full-day/images/3-reload.png rename to content/images/3-reload.png diff --git a/content/full-day/images/3-secrets-variables.png b/content/images/3-secrets-variables.png similarity index 100% rename from content/full-day/images/3-secrets-variables.png rename to content/images/3-secrets-variables.png diff --git a/content/full-day/images/4-select-file.png b/content/images/4-select-file.png similarity index 100% rename from content/full-day/images/4-select-file.png rename to content/images/4-select-file.png diff --git a/content/full-day/images/5-copilot-chat-references.png b/content/images/5-copilot-chat-references.png similarity index 100% rename from content/full-day/images/5-copilot-chat-references.png rename to content/images/5-copilot-chat-references.png diff --git a/content/full-day/images/7-generate-commit-message.png b/content/images/7-generate-commit-message.png similarity index 100% rename from content/full-day/images/7-generate-commit-message.png rename to content/images/7-generate-commit-message.png diff --git a/content/prompts/README.md b/content/prompts/README.md deleted file mode 100644 index 83f3db0..0000000 --- a/content/prompts/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Pets Workshop Prompts - -This directory contains various prompts designed for different aspects of development and enhancement of the Pets Workshop project. These prompts are meant for illustration purposes only. - -## Prompt Overview - -### Interface and User Experience - -- **[fun-add-themes](./fun-add-themes.md)**: Adds a theme selector dropdown that allows users to switch between multiple visual themes including 80s Retro, Terminal Classic, Hand-Sketched, Steampunk, and Fantasy Realm. Enhances user customization and visual appeal. - -- **[fun-add-dog-animation](./fun-add-dog-animation.md)**: Implements an interactive cartoon dog animation in the bottom-right corner of the website that follows the user's cursor with its eyes. The dog remains visible while scrolling and has extra animations on mouse clicks, adding a playful element to the user experience. - -### Backend Development - -- **[conversion-convert-flask-to-golang](./conversion-convert-flask-to-golang.md)**: Provides instructions for migrating the existing Python Flask server to a Go-based implementation while maintaining identical functionality, API endpoints, and response formats. The goal is to create a functionally equivalent server using Go's standard library. - -- **[monitoring-add-logging](./monitoring-add-logging.md)**: Details requirements for implementing a comprehensive logging system in the Python Flask server with multiple logging levels, consistent formatting, configuration options, and performance considerations. This improves monitoring, debugging, and operational visibility. diff --git a/content/prompts/conversion-convert-flask-to-golang.md b/content/prompts/conversion-convert-flask-to-golang.md deleted file mode 100644 index c8b3c9d..0000000 --- a/content/prompts/conversion-convert-flask-to-golang.md +++ /dev/null @@ -1,24 +0,0 @@ -# Flask to Go Server Migration Project - -## Objective - -Convert the existing Python Flask server implementation to a Go-based server with identical functionality and API endpoints. The Go implementation should maintain the same request handling, routes, data processing, and response formats as the original Flask server. - -The Python Flask is stored in #folder:server - -## Requirements -1. Create a functionally equivalent Go server implementation -2. Match all existing API endpoints, query parameters, and HTTP methods -3. Preserve all current data processing logic and response formats -4. Implement the same error handling and status codes -5. Maintain any authentication mechanisms present in the Flask implementation -6. Use only the Go standard library where possible, with minimal external dependencies -7. Include appropriate comments explaining the code and any implementation decisions - -## Deliverables -1. Complete Go source code organized in a folder named `go_server` -2. A main.go file with server initialization and configuration -3. Separate handler files for different API endpoint groups -4. Any utility or helper functions required -5. A README.md with setup and usage instructions - diff --git a/content/prompts/fun-add-dog-animation.md b/content/prompts/fun-add-dog-animation.md deleted file mode 100644 index 43852a0..0000000 --- a/content/prompts/fun-add-dog-animation.md +++ /dev/null @@ -1,13 +0,0 @@ -# Puppy Cursor Follower - -Add an adorable cartoon dog to the bottom-right corner of the website that follows the user's cursor with its eyes, similar to the classic XEyes program from X11. - -## Requirements: -- The dog should be cute and cartoony with expressive eyes -- Eyes should smoothly track the cursor position across the entire screen -- Position the dog in the bottom-right corner as a fixed element (sticky positioning) -- Dog should remain visible even when the page is scrolled -- Add a slight head tilt or ear wiggle on mouse clicks for extra charm -- Optional: Make the dog occasionally blink or perform a random animation - -Let's make browsing fun again with this interactive canine companion! 🐶 \ No newline at end of file diff --git a/content/prompts/fun-add-themes.md b/content/prompts/fun-add-themes.md deleted file mode 100644 index a0611b4..0000000 --- a/content/prompts/fun-add-themes.md +++ /dev/null @@ -1,40 +0,0 @@ -# 🎨 Theme-tastic Interface Enhancement! - -## 🎯 Your Mission -Transform our boring interface into a playground of visual delights! Let users express themselves through awesome themes. - -## 🔍 Key Requirements -1. **Theme Selector Dropdown** - - Position: ↗️ Top-right corner of the screen - - Behavior: Interface instantly refreshes when a new theme is selected - - Default label: "Default" (our current look) - -## 🌈 Required Themes -Add these fabulous theme options: - -* **80s Retro** 🕹️ - - Think neon colors, bold patterns, geometric shapes - - Inspiration: Miami Vice, arcade games, synthwave - -* **Terminal Classic** 💻 - - Nostalgic VT100 green-on-black terminal look - - Features: Monospace fonts, scan lines, command prompt aesthetic - -* **Hand-Sketched** ✏️ - - UI elements that appear hand-drawn with a playful, creative feel - - Think: Doodles, sketch lines, paper texture backgrounds - -* **Steampunk** ⚙️ - - Brass, gears, leather, and Victorian-era aesthetics mixed with futuristic elements - - Inspiration: Jules Verne, The League of Extraordinary Gentlemen, Bioshock Infinite - -* **Fantasy Realm** 🧙 - - Mystical forests, glowing runes, and enchanted elements - - Inspiration: Lord of the Rings, Dungeons & Dragons, Skyrim - - -## 🚀 Bonus Points -- Add subtle animations for theme transitions -- Include a small preview of each theme in the dropdown -- Make sure all themes maintain accessibility standards - diff --git a/content/prompts/monitoring-add-logging.md b/content/prompts/monitoring-add-logging.md deleted file mode 100644 index 75aaef1..0000000 --- a/content/prompts/monitoring-add-logging.md +++ /dev/null @@ -1,30 +0,0 @@ -Add logging commands to server application which is written in python - -The Python Flask is stored in #folder:server - -Create a standardized logging system for the Python Flask with the following requirements: - -1. LOGGING LEVELS: Implement five distinct logging levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) with clear usage guidelines for each. - -2. FORMAT CONSISTENCY: Define a consistent log entry format including: - - Timestamp (ISO 8601 format: YYYY-MM-DD HH:MM:SS.mmm) - - Log level - - Module/component name - - Thread ID (where applicable) - - Message content - -3. CONFIGURATION: Provide a configuration system that allows: - - Setting global minimum log level - - Per-module logging levels - - Multiple output destinations (console, file, external service) - - Log rotation settings for file outputs - -4. CODE EXAMPLES: Include example implementations showing: - - Proper logger initialization - - Correct usage of each log level - - Error/exception logging with stack traces - - Context-enriched logging - -5. PERFORMANCE CONSIDERATIONS: Address how to optimize logging for production environments. - -The solution should be maintainable, follow industry best practices, and minimize performance impact. diff --git a/scripts/run-e2e-tests.sh b/scripts/run-e2e-tests.sh new file mode 100755 index 0000000..52b3e6a --- /dev/null +++ b/scripts/run-e2e-tests.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# run-e2e-tests.sh - Runs Playwright end-to-end tests for the client +# +# Usage: +# ./scripts/run-e2e-tests.sh # Run all tests +# ./scripts/run-e2e-tests.sh --headed # Run with browser UI +# ./scripts/run-e2e-tests.sh --project=chromium # Run only chromium tests +# ./scripts/run-e2e-tests.sh e2e-tests/homepage.spec.ts # Run specific test file +# +# This script handles: +# - Installing dependencies if needed +# - Installing Playwright browsers +# - Running tests with the application servers started automatically +# - Passing through any arguments to the Playwright test command + +# Define color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Store initial directory +INITIAL_DIR=$(pwd) + +# Function to handle errors +handle_error() { + echo -e "${RED}Error: $1${NC}" + cd "$INITIAL_DIR" + exit 1 +} + +echo -e "${GREEN}Running Playwright end-to-end tests...${NC}" + +# Check if we're in scripts directory and navigate accordingly +if [[ $(basename $(pwd)) == "scripts" ]]; then + SCRIPT_DIR=$(pwd) + cd .. +else + SCRIPT_DIR="./scripts" +fi + +# Verify we're in the project root +if [[ ! -d "client" ]]; then + handle_error "Not in project root directory. Please run from the project root or scripts directory." +fi + +# Navigate to client directory +cd client || handle_error "client directory not found" + +# Check if node_modules exists +if [[ ! -d "node_modules" ]]; then + echo -e "${YELLOW}Dependencies not found. Installing dependencies...${NC}" + if ! npm install; then + handle_error "Failed to install dependencies" + fi +fi + +# Check if Playwright browsers are installed +echo -e "${YELLOW}Ensuring Playwright browsers are installed...${NC}" +if ! npx playwright install --with-deps chromium; then + handle_error "Failed to install Playwright browsers" +fi + +echo -e "${YELLOW}Running end-to-end tests...${NC}" + +# Run tests with configurable options +# Default to running all tests unless specific options are passed +if [[ $# -eq 0 ]]; then + # No arguments, run all tests + if npm run test:e2e; then + echo -e "${GREEN}All e2e tests passed!${NC}" + EXIT_CODE=0 + else + echo -e "${RED}Some e2e tests failed!${NC}" + EXIT_CODE=1 + fi +else + # Pass through any arguments to the test command + echo -e "${YELLOW}Running tests with options: $@${NC}" + if npx playwright test "$@"; then + echo -e "${GREEN}E2E tests completed successfully!${NC}" + EXIT_CODE=0 + else + echo -e "${RED}E2E tests failed!${NC}" + EXIT_CODE=1 + fi +fi + +# Return to initial directory +cd "$INITIAL_DIR" + +exit $EXIT_CODE diff --git a/scripts/run-server-tests.sh b/scripts/run-server-tests.sh new file mode 100755 index 0000000..10b4cbf --- /dev/null +++ b/scripts/run-server-tests.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# run-tests.sh - Runs Python unit tests for the server + +# Define color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Store initial directory +INITIAL_DIR=$(pwd) + +# Function to handle errors +handle_error() { + echo -e "${RED}Error: $1${NC}" + cd "$INITIAL_DIR" + exit 1 +} + +echo -e "${GREEN}Running Python unit tests...${NC}" + +# Check if we're in scripts directory and navigate accordingly +if [[ $(basename $(pwd)) == "scripts" ]]; then + SCRIPT_DIR=$(pwd) + cd .. +else + SCRIPT_DIR="./scripts" +fi + +# Verify we're in the project root +if [[ ! -d "server" ]]; then + handle_error "Not in project root directory. Please run from the project root or scripts directory." +fi + +# Check if virtual environment exists +if [[ ! -d "server/venv" ]]; then + echo -e "${YELLOW}Virtual environment not found. Setting up environment first...${NC}" + if ! bash "$SCRIPT_DIR/setup-environment.sh"; then + handle_error "Failed to set up environment" + fi +fi + +echo -e "${YELLOW}Activating virtual environment...${NC}" + +# Navigate to server directory +cd server || handle_error "server directory not found" + +# Activate virtual environment +if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then + # Windows + if ! source venv/Scripts/activate; then + handle_error "Failed to activate Python virtual environment" + fi +else + # macOS/Linux + if ! source venv/bin/activate; then + handle_error "Failed to activate Python virtual environment" + fi +fi + +echo -e "${YELLOW}Running tests...${NC}" + +# Run tests with verbose output +if python -m unittest; then + echo -e "${GREEN}All tests passed!${NC}" + EXIT_CODE=0 +else + echo -e "${RED}Some tests failed!${NC}" + EXIT_CODE=1 +fi + +# Deactivate virtual environment +deactivate + +# Return to initial directory +cd "$INITIAL_DIR" + +exit $EXIT_CODE diff --git a/scripts/setup-environment.sh b/scripts/setup-environment.sh new file mode 100755 index 0000000..cb848ed --- /dev/null +++ b/scripts/setup-environment.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# setup-environment.sh - Sets up Python virtual environment and installs all dependencies + +# Define color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Store initial directory +INITIAL_DIR=$(pwd) + +# Function to handle errors +handle_error() { + echo -e "${RED}Error: $1${NC}" + cd "$INITIAL_DIR" + exit 1 +} + +echo -e "${GREEN}Setting up development environment...${NC}" + +# Check if we're in scripts directory and navigate accordingly +if [[ $(basename $(pwd)) == "scripts" ]]; then + cd .. +fi + +# Verify we're in the project root +if [[ ! -d "server" ]] || [[ ! -d "client" ]]; then + handle_error "Not in project root directory. Please run from the project root or scripts directory." +fi + +echo -e "${YELLOW}Setting up Python virtual environment...${NC}" + +# Check OS and use appropriate Python command +if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then + # Windows + if ! py -m venv server/venv; then + handle_error "Failed to create Python virtual environment" + fi + + if ! source server/venv/Scripts/activate; then + handle_error "Failed to activate Python virtual environment" + fi +else + # macOS/Linux + if ! python3 -m venv server/venv; then + handle_error "Failed to create Python virtual environment" + fi + + if ! source server/venv/bin/activate; then + handle_error "Failed to activate Python virtual environment" + fi +fi + +echo -e "${YELLOW}Installing Python dependencies...${NC}" +if ! pip install -r server/requirements.txt; then + handle_error "Failed to install Python dependencies" +fi + +echo -e "${YELLOW}Installing npm dependencies...${NC}" +cd client || handle_error "client directory not found" + +if ! npm install; then + handle_error "Failed to install npm dependencies" +fi + +cd .. + +echo -e "${GREEN}Environment setup completed successfully!${NC}" +echo -e "${GREEN}Python virtual environment: server/venv${NC}" +echo -e "${GREEN}All dependencies installed.${NC}" +echo "" +echo -e "${YELLOW}To activate the Python environment manually:${NC}" +if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then + echo " source server/venv/Scripts/activate" +else + echo " source server/venv/bin/activate" +fi + +# Return to initial directory +cd "$INITIAL_DIR" diff --git a/scripts/start-app.ps1 b/scripts/start-app.ps1 deleted file mode 100644 index ec26091..0000000 --- a/scripts/start-app.ps1 +++ /dev/null @@ -1,89 +0,0 @@ -# Define color codes for PowerShell -$Green = [System.ConsoleColor]::Green -$DefaultColor = [System.ConsoleColor]::White - -# Store initial directory -$InitialDir = Get-Location - -# Check if we're in scripts directory and navigate accordingly -if ((Split-Path -Path (Get-Location) -Leaf) -eq "scripts") { - Set-Location .. -} - -Write-Host "Starting API (Flask) server..." - -# Create and activate virtual environment -if (-not (Test-Path venv)) { - python -m venv venv -} -if ($IsWindows) { - & ./venv/Scripts/Activate.ps1 -} else { - & bash -c "source ./venv/bin/activate" -} - -Set-Location server -ErrorAction Stop -pip install -r requirements.txt -Set-Location .. -$env:FLASK_DEBUG = 1 -$env:FLASK_PORT = 5100 - - -# Start Python server -$pythonProcess = Start-Process python ` - -WorkingDirectory (Join-Path $PSScriptRoot "..\server") ` - -ArgumentList "app.py" ` - -PassThru ` - -NoNewWindow - -Write-Host "Starting client (Astro)..." -Set-Location client -ErrorAction Stop -npm install -cd .. -if ($IsWindows) { - $npcCmd = "npm.cmd" -} else { - $npcCmd = "npm" -} - -$clientProcess = Start-Process "$npcCmd" ` - -WorkingDirectory (Join-Path $PSScriptRoot "..\client") ` - -ArgumentList "run", "dev", "--", "--no-clearScreen" ` - -PassThru ` - -NoNewWindow - -# Sleep for 5 seconds -Start-Sleep -Seconds 5 - -# Display the server URLs -Write-Host "`nServer (Flask) running at: http://localhost:5100" -ForegroundColor $Green -Write-Host "Client (Astro) server running at: http://localhost:4321`n" -ForegroundColor $Green -Write-Host "Ctrl+C to stop the servers" - -# Function to handle cleanup -function Cleanup { - Write-Host "Shutting down servers..." - - # Kill processes and their child processes - if ($pythonProcess) { Stop-Process -Id $pythonProcess.Id -Force -ErrorAction SilentlyContinue } - if ($clientProcess) { Stop-Process -Id $clientProcess.Id -Force -ErrorAction SilentlyContinue } - - # Deactivate virtual environment if it exists - if (Test-Path Function:\deactivate) { - deactivate - } - - # Return to initial directory - Set-Location $InitialDir - exit -} - -# Register cleanup for script termination -$null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { Cleanup } - -try { - # Keep the script running until Ctrl+C - Wait-Process -Id $pythonProcess.Id -} finally { - Cleanup -} diff --git a/scripts/start-app.sh b/scripts/start-app.sh index 8362994..b0f2cea 100755 --- a/scripts/start-app.sh +++ b/scripts/start-app.sh @@ -2,6 +2,7 @@ # Define color codes GREEN='\033[0;32m' +YELLOW='\033[1;33m' NC='\033[0m' # No Color # Store initial directory @@ -9,23 +10,32 @@ INITIAL_DIR=$(pwd) # Check if we're in scripts directory and navigate accordingly if [[ $(basename $(pwd)) == "scripts" ]]; then + SCRIPT_DIR=$(pwd) cd .. +else + SCRIPT_DIR="./scripts" +fi + +# Check if environment is already set up, if not, run setup +if [[ ! -d "server/venv" ]] || [[ ! -d "client/node_modules" ]]; then + echo -e "${YELLOW}Environment not set up. Running setup script...${NC}" + if ! bash "$SCRIPT_DIR/setup-environment.sh"; then + echo "Setup failed. Exiting." + cd "$INITIAL_DIR" + exit 1 + fi fi echo "Starting API (Flask) server..." -# Check OS and use appropriate Python command +# Activate virtual environment if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then # Windows - py -m venv venv - source venv/Scripts/activate || . venv/Scripts/activate + source server/venv/Scripts/activate || . server/venv/Scripts/activate else # macOS/Linux - python3 -m venv venv - source venv/bin/activate || . venv/bin/activate + source server/venv/bin/activate || . server/venv/bin/activate fi - -pip install -r server/requirements.txt cd server || { echo "Error: server directory not found" cd "$INITIAL_DIR" @@ -50,7 +60,8 @@ cd ../client || { cd "$INITIAL_DIR" exit 1 } -npm install + +# npm packages should already be installed by setup script npm run dev -- --no-clearScreen & # Store the SvelteKit server process ID diff --git a/server/app.py b/server/app.py index b6331cc..b960804 100644 --- a/server/app.py +++ b/server/app.py @@ -65,7 +65,5 @@ def get_dog(id: int) -> tuple[Response, int] | Response: return jsonify(dog) -## HERE - if __name__ == '__main__': app.run(debug=True, port=5100) # Port 5100 to avoid macOS conflicts \ No newline at end of file diff --git a/server/tests/__init__.py b/server/tests/__init__.py new file mode 100644 index 0000000..255ec74 --- /dev/null +++ b/server/tests/__init__.py @@ -0,0 +1 @@ +# This file makes the tests directory a Python package for test discovery diff --git a/server/test_app.py b/server/tests/test_app.py similarity index 82% rename from server/test_app.py rename to server/tests/test_app.py index adbdf5f..82243fe 100644 --- a/server/test_app.py +++ b/server/tests/test_app.py @@ -91,6 +91,23 @@ def test_get_dogs_structure(self, mock_query): self.assertEqual(len(data), 1) self.assertEqual(set(data[0].keys()), {'id', 'name', 'breed'}) + @patch('app.db.session.query') + def test_get_dog_not_found(self, mock_query): + """Test 404 error when dog is not found by ID""" + # Arrange + mock_query_instance = MagicMock() + mock_query.return_value = mock_query_instance + mock_query_instance.join.return_value = mock_query_instance + mock_query_instance.filter.return_value = mock_query_instance + mock_query_instance.first.return_value = None # Simulate dog not found + + # Act + response = self.app.get('/api/dogs/999') + + # Assert + self.assertEqual(response.status_code, 404) + data = json.loads(response.data) + self.assertEqual(data['error'], "Dog not found") if __name__ == '__main__': unittest.main() \ No newline at end of file