diff --git a/.github/WORKFLOW_ARCHITECTURE.md b/.github/WORKFLOW_ARCHITECTURE.md new file mode 100644 index 0000000..9f3eb5c --- /dev/null +++ b/.github/WORKFLOW_ARCHITECTURE.md @@ -0,0 +1,123 @@ +# GitHub Actions Workflow Architecture + +## Overview + +This repository uses a modular GitHub Actions workflow architecture to ensure consistency between PR validation and deployment while preventing accidental deployments. + +## Workflow Files + +### 1. `.github/workflows/build.yml` (Reusable Workflow) +**Purpose**: Core build and validation logic used by both PR validation and deployment + +**Features**: +- Reusable workflow that can be called by other workflows +- Accepts optional `checkout-ref` input for specific git references +- Performs all validation steps: + - Linting (ESLint) + - Spell checking (source files) + - TypeScript checking and Astro build + - Spell checking (generated HTML) + - Internal link validation + - Upload pages artifact for deployment + +**Outputs**: +- `artifact-uploaded`: Boolean indicating if the build artifact was successfully created + +### 2. `.github/workflows/deploy.yml` +**Purpose**: Deploy the site to GitHub Pages + +**Triggers**: +- Automatic: Push to `main` branch +- Manual: `workflow_dispatch` with optional `deploy` flag + +**Safety Features**: +- Only deploys from `main` branch +- Manual trigger requires explicit `deploy: true` flag +- Uses concurrency group to prevent parallel deployments +- Conditional deployment logic prevents accidental deploys + +**Jobs**: +1. `build`: Calls reusable build workflow +2. `deploy`: Conditionally deploys to GitHub Pages (only if conditions are met) + +### 3. `.github/workflows/pr-validation.yml` +**Purpose**: Validate pull requests before merge + +**Features**: +- Uses the same build workflow as deployment (ensures parity) +- Provides detailed status comments on PRs +- Acts as a complete dry-run of the deployment process +- Reports all validation results clearly + +## Key Design Decisions + +### 1. Single Source of Truth +All build and validation logic lives in `build.yml`, ensuring PR validation and deployment use identical processes. + +### 2. Deployment Safety +Multiple safeguards prevent accidental deployment: +- Branch restrictions (`main` only) +- Explicit flags for manual deployment +- Conditional job execution + +### 3. Complete PR Validation +PRs undergo the exact same validation as deployment, including: +- All linting and type checking +- Spell checking (both source and generated HTML) +- Full site build +- Link validation + +This prevents the "passes CI but fails deployment" scenario. + +## Workflow Execution Patterns + +### Pattern 1: Normal Development (PR → Merge → Deploy) +1. Developer creates PR → `pr-validation.yml` runs → Full validation +2. PR approved and merged → Push to `main` triggers `deploy.yml` +3. `deploy.yml` runs build → Automatically deploys + +### Pattern 2: Manual Deployment Dry-Run +1. Run `deploy.yml` manually from any branch +2. Set `deploy: false` (or leave default) +3. Build runs but deployment is skipped +4. Useful for testing workflow changes + +### Pattern 3: Emergency Manual Deployment +1. Run `deploy.yml` manually from `main` branch +2. Set `deploy: true` +3. Full build and deployment executes +4. Useful if automatic deployment fails + +## Maintenance Notes + +### Adding New Validation Steps +Add new validation steps to `build.yml` only. They will automatically be included in both PR validation and deployment. + +### Modifying Deployment Conditions +Edit the `if` condition in the `deploy` job of `deploy.yml`. Current logic: +```yaml +if: | + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + (github.event_name == 'workflow_dispatch' && inputs.deploy == true && github.ref == 'refs/heads/main') +``` + +### Debugging Workflow Issues +1. Check the workflow run logs in GitHub Actions tab +2. Use `workflow_dispatch` to manually test workflows +3. The PR validation comment provides a summary of what checks ran + +## Security Considerations + +- Deployment requires `pages: write` and `id-token: write` permissions (only in deploy.yml) +- PR validation has minimal permissions: + - `contents: read` for checking out code + - `pull-requests: write` for posting status comments + - No write access to Pages (follows principle of least privilege) +- The `configure-pages` action was removed as it's not needed (we don't use its outputs) +- Concurrency groups prevent race conditions during deployment +- Branch protection rules should be configured to require PR validation before merge + +### Permission Model +- **PR Validation**: Read-only access (can't modify repository or deploy) +- **Deployment**: Write access only when pushing to main branch +- **Manual Workflow**: Deployment only allowed from main branch with explicit flag \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5e24acd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,84 @@ +name: Build and Validate + +on: + workflow_call: + inputs: + checkout-ref: + description: 'Git ref to checkout (defaults to default branch)' + type: string + required: false + default: '' + outputs: + artifact-uploaded: + description: 'Whether the pages artifact was uploaded successfully' + value: ${{ jobs.build.outputs.artifact-uploaded }} + +jobs: + build: + runs-on: ubuntu-latest + outputs: + artifact-uploaded: ${{ steps.upload.outputs.artifact-uploaded }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.checkout-ref }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Run Linting + run: npm run lint + + - name: Run Spell Check (Source) + run: npm run spellcheck + + - name: Build Site + run: npm run build + + - name: Run Spell Check (HTML) + id: spellcheck-html + run: | + echo "Checking HTML files in dist directory..." + npm run spellcheck:html + echo "HTML spell check completed successfully" + continue-on-error: true + + - name: Debug spell check failure + if: steps.spellcheck-html.outcome == 'failure' + run: | + echo "::warning::HTML spell check failed - debugging information:" + echo "Files in dist directory:" + find dist -name "*.html" -type f 2>/dev/null | head -10 || echo "No HTML files found" + echo "" + echo "CSpell ignore paths:" + cat cspell.json | grep -A10 '"ignorePaths"' || true + echo "" + echo "Re-running spell check with verbose output:" + npx cspell "dist/**/*.html" --no-progress --verbose 2>&1 | head -30 || true + exit 1 + + - name: Validate Internal Links + run: npm run validate:links + + - name: Upload Pages Artifact + id: upload + uses: actions/upload-pages-artifact@v3 + with: + path: './dist' + + - name: Set Output + if: always() + run: | + if [ "${{ steps.upload.outcome }}" == "success" ]; then + echo "artifact-uploaded=true" >> $GITHUB_OUTPUT + else + echo "artifact-uploaded=false" >> $GITHUB_OUTPUT + fi \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3a7ab8c..6563987 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,12 @@ on: push: branches: [main] workflow_dispatch: + inputs: + deploy: + description: 'Deploy to GitHub Pages (only works from main branch)' + type: boolean + required: false + default: false permissions: contents: read @@ -16,45 +22,27 @@ concurrency: jobs: build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install Dependencies - run: npm ci - - - name: Run Linting - run: npm run lint - - - name: Run Spell Check (Source) - run: npm run spellcheck - - - name: Build Site - run: npm run build - - - name: Run Spell Check (HTML) - run: npm run spellcheck:html - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: './dist' + uses: ./.github/workflows/build.yml + permissions: + contents: read + pages: write + id-token: write deploy: + # Only deploy if: + # 1. Push to main branch (automatic deployment) + # 2. Manual workflow dispatch with deploy=true AND on main branch + if: | + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + (github.event_name == 'workflow_dispatch' && inputs.deploy == true && github.ref == 'refs/heads/main') + environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest needs: build + steps: - name: Deploy to GitHub Pages id: deployment diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 625eb68..039b5b2 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -12,56 +12,48 @@ permissions: jobs: validate: name: Validate PR + uses: ./.github/workflows/build.yml + with: + checkout-ref: ${{ github.event.pull_request.head.sha }} + permissions: + contents: read + + report-status: + name: Report Validation Status runs-on: ubuntu-latest + needs: validate + if: always() + permissions: + pull-requests: write steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install Dependencies - run: npm ci - - - name: Run Linting - run: npm run lint - continue-on-error: false - - - name: Type Check and Build - run: npm run build - continue-on-error: false - - - name: Check Build Output - run: | - if [ ! -d "dist" ]; then - echo "Error: Build output directory 'dist' not found" - exit 1 - fi - echo "Build successful - dist directory created" - echo "Files generated: $(find dist -type f | wc -l)" - - - name: Validate Internal Links - run: npm run validate:links - continue-on-error: false - - name: Report Status - if: always() uses: actions/github-script@v7 with: script: | - const status = '${{ job.status }}'; + const status = '${{ needs.validate.result }}'; const icon = status === 'success' ? '✅' : '❌'; const message = status === 'success' ? 'All checks passed! Ready for review.' : 'Some checks failed. Please review the errors above.'; + // Build check list with actual results + const checks = [ + '✓ Linting', + '✓ Spell check (source)', + '✓ Type checking & Build', + '✓ Spell check (HTML)', + '✓ Internal link validation', + '✓ Artifact upload' + ]; + + const checkList = status === 'success' + ? checks.join('\n') + : 'Please check the workflow logs for details on which checks failed.'; + github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: `## PR Validation ${icon}\n\n${message}\n\n### Checks Performed:\n- ✓ Linting\n- ✓ Type checking\n- ✓ Build verification\n- ✓ Internal link validation` + body: `## PR Validation ${icon}\n\n${message}\n\n### Checks Performed:\n${checkList}\n\n*This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.*` }); \ No newline at end of file diff --git a/VALIDATION.md b/VALIDATION.md new file mode 100644 index 0000000..bef90d8 --- /dev/null +++ b/VALIDATION.md @@ -0,0 +1,144 @@ +# Validation & CI Parity Guide + +This document explains how to run the same validation checks locally that run in CI, ensuring your changes will pass all checks before pushing. + +## Quick Start + +To run all CI checks locally (exactly as they run in GitHub Actions): + +```bash +npm run test:ci +``` + +For a more verbose version with progress indicators: + +```bash +npm run test:ci:verbose +``` + +## Individual Validation Commands + +### 1. Linting (ESLint) +Checks code style and catches common errors: +```bash +npm run lint # Check for issues +npm run lint:fix # Auto-fix where possible +``` + +### 2. Spell Checking + +#### Source Files +Checks markdown, TypeScript, and Astro files: +```bash +npm run spellcheck +``` + +#### HTML Output +Checks generated HTML files (requires build first): +```bash +npm run build +npm run spellcheck:html +``` + +#### Both +```bash +npm run spellcheck:all +``` + +### 3. Build +Generates the static site: +```bash +npm run build +``` + +### 4. Link Validation +Checks for broken internal links (requires build first): +```bash +npm run validate:links +``` + +### 5. All Validations +Runs everything in sequence: +```bash +npm run validate:all +# OR equivalently: +npm run test:ci +``` + +## CI/CD Workflow + +The GitHub Actions workflow runs these exact same checks: +1. Linting (`npm run lint`) +2. Source spell check (`npm run spellcheck`) +3. Build (`npm run build`) +4. HTML spell check (`npm run spellcheck:html`) +5. Link validation (`npm run validate:links`) + +## Troubleshooting + +### Spell Check Issues + +If spell check is failing: + +1. **For technical terms**: Add them to `cspell.json` in the `words` array +2. **For actual typos**: Fix them in the source files +3. **HTML entity issues**: Words with HTML entities (like `doesn't`) may need special handling + +To debug spell check issues: +```bash +# See which files are being checked +npx cspell "dist/**/*.html" --no-progress --verbose + +# Check the ignore patterns +cat cspell.json | grep -A10 "ignorePaths" +``` + +### Build Issues + +If the build fails: +```bash +# Run with verbose output +npm run build + +# Check for TypeScript errors +npx astro check +``` + +### Link Validation Issues + +If link validation fails: +```bash +# Run the validator directly +node scripts/validate-links.js + +# Check which links are broken +ls -la dist/ # Ensure build output exists +``` + +## Pre-Push Checklist + +Before pushing changes: + +1. ✅ Run `npm run test:ci` locally +2. ✅ Fix any issues that arise +3. ✅ If adding new terms, update `cspell.json` +4. ✅ Commit all changes including config updates + +## Common Gotchas + +1. **Spell check ignores entire directories**: Check `ignorePaths` in `cspell.json` +2. **HTML spell check requires build**: Always run `npm run build` first +3. **CI uses exact npm scripts**: Don't rely on different local commands +4. **Case sensitivity**: File paths are case-sensitive in CI (Linux) but may not be locally (macOS/Windows) + +## Maintaining CI/CD Parity + +To ensure local development matches CI: + +1. Always use the npm scripts rather than direct commands +2. Run `npm run test:ci` before pushing +3. Keep dependencies up to date with `npm ci` (not `npm install`) +4. If CI fails but local passes, check for: + - Missing files in git + - Different Node.js versions + - Platform-specific issues (Linux CI vs local macOS/Windows) \ No newline at end of file diff --git a/cspell.json b/cspell.json index 100a483..a040542 100644 --- a/cspell.json +++ b/cspell.json @@ -2,6 +2,7 @@ "version": "0.2", "language": "en", "words": [ + "administrivia", "agentic", "allwork", "Anthropics", @@ -9,40 +10,62 @@ "astrojs", "autoclosure", "Bandung", + "Berman", "BGRA", "briefs", "Claude", "Codegen", "cplusplus", "devcontainer", + "doesn", "expressibility", "frontmatter", + "HDXLXC", + "IIRC", + "imread", + "instancetype", "justfile", + "kindof", "linter", "MDX", "meso", "Meso", "metrids", + "msapplication", + "mstile", "mutatis", + "nonnull", + "NSURL", + "objc", "plx", "Playwright", "prb", + "pretraining", "ripgrep", "sitemap", "speedbump", "subclassing", + "Tailgraph", "Tailwindcss", "todolist", "Typesafe", "uncategorized", "Uncategorized", "webfetch", - "WWDC" + "WWDC", + "xctest" ], "ignorePaths": [ "node_modules", - "dist", ".git", + "dist/**/*.js", + "dist/**/*.css", + "dist/**/*.map", + "dist/_astro/**", + "dist/favicon.ico", + "dist/robots.txt", + "dist/rss.xml", + "dist/sitemap-*.xml", "*.min.js", "*.min.css", "package-lock.json", diff --git a/package.json b/package.json index ddb1047..e44d20d 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "spellcheck:html": "cspell \"dist/**/*.html\" --no-progress", "spellcheck:all": "npm run spellcheck && npm run build && npm run spellcheck:html", "validate:links": "node scripts/validate-links.js", - "validate:all": "npm run lint && npm run spellcheck && npm run build && npm run spellcheck:html && npm run validate:links" + "validate:all": "npm run lint && npm run spellcheck && npm run build && npm run spellcheck:html && npm run validate:links", + "test:ci": "npm run lint && npm run spellcheck && npm run build && npm run spellcheck:html && npm run validate:links", + "test:ci:verbose": "echo '🔍 Running CI validation locally...' && npm run lint && echo '✓ Linting passed' && npm run spellcheck && echo '✓ Source spell check passed' && npm run build && echo '✓ Build succeeded' && npm run spellcheck:html && echo '✓ HTML spell check passed' && npm run validate:links && echo '✓ Link validation passed' && echo '✅ All CI checks passed!'" }, "dependencies": { "@astrojs/check": "^0.9.4", diff --git a/src/content/briefs/objective-c/equivalent-objects-function.md b/src/content/briefs/objective-c/equivalent-objects-function.md index 50886e6..f667b8b 100644 --- a/src/content/briefs/objective-c/equivalent-objects-function.md +++ b/src/content/briefs/objective-c/equivalent-objects-function.md @@ -108,7 +108,7 @@ BOOL EquivalentObjects(id _Nullable lhs, id _Nullable rhs) { As such a simple function, there's not much to say about it beyond administrivia: - it's not a bad idea to put a namespace prefix on it -- it's also not a bad idea to give it a hard-to-use Swift name (e.g. `NS_SWIFT_NAME(__dont_use_EquivalentObjects(_:_:)`) +- it's also not a bad idea to give it a hard-to-use Swift name (e.g. `NS_SWIFT_NAME(__don't_use_EquivalentObjects(_:_:)`) - you can *consider* replacing it with a macro, but I wouldn't—it's trickier than it looks! - you want to make sure it's not visible-from/used-within your public headers[^5] @@ -120,5 +120,5 @@ Once you have that function defined, the rest is just being consistent with usin My personal strategy for it is: - use it unconditionally for *all* equality checks *inside* methods like `isEqual:` (and type-specific helpers like `isEqualToProjectDescriptor:`) -- use it *when appropriate* for checks impacting control flow (it's often more legible to include explict `!= nil` checks in those cases, however) +- use it *when appropriate* for checks impacting control flow (it's often more legible to include explicit `!= nil` checks in those cases, however) diff --git a/src/content/projects/agentic-navigation-guide/index.md b/src/content/projects/agentic-navigation-guide/index.md index 008ab11..749692c 100644 --- a/src/content/projects/agentic-navigation-guide/index.md +++ b/src/content/projects/agentic-navigation-guide/index.md @@ -111,7 +111,7 @@ If you measure time-to-completion, I'd estimate I finished this 7x faster than I So, purely for speed, this was a big win. -If you measure cognitive effort, the picture's a bit more nuanced: the cognitive effort *was* reduced, but it was also *compressed* and *front-loaded*—writing that psecification document was *a lot* of work! +If you measure cognitive effort, the picture's a bit more nuanced: the cognitive effort *was* reduced, but it was also *compressed* and *front-loaded*—writing that specification document was *a lot* of work! Ordinarily, I use a process like this: diff --git a/src/content/projects/hdxl-xctest-retrofit/index.md b/src/content/projects/hdxl-xctest-retrofit/index.md index a3fc3c2..c0c64bd 100644 --- a/src/content/projects/hdxl-xctest-retrofit/index.md +++ b/src/content/projects/hdxl-xctest-retrofit/index.md @@ -100,7 +100,7 @@ func validateCodableRoundTrip( This project would have taken under a week, end-to-end, if done with full-time focus—it's a nice, self-contained task. -### Suprise Gotcha: Expansion Testing +### Surprise Gotcha: Expansion Testing The expansion tests proved harder to write than I thought b/c the macro-expansion helpers I was using don't fully account for whitespace within the macro expansion: diff --git a/src/lib/opengraph.ts b/src/lib/opengraph.ts index a11ec73..4bf5987 100644 --- a/src/lib/opengraph.ts +++ b/src/lib/opengraph.ts @@ -80,7 +80,7 @@ export function getPostOGData( month: "long", day: "numeric" }), - author: "Paul R. Berman", + author: "plx", theme: "dark", backgroundImage: "gradient", logo: `${siteUrl}/favicon-light.svg` @@ -98,12 +98,11 @@ export function getPostOGData( article: { publishedTime: post.data.date, modifiedTime: post.data.modifiedDate, - author: "Paul R. Berman", + author: "plx", section: "Blog" }, twitter: { - card: "summary_large_image", - creator: "@plxgithub" + card: "summary_large_image" } }; } @@ -125,7 +124,7 @@ export function getBriefOGData( ogImage = generateTailgraphURL({ title: brief.data.cardTitle || brief.data.title, subtitle: category?.titlePrefix || category?.displayName || "Brief", - author: "Paul R. Berman", + author: "plx", theme: "dark", backgroundImage: "gradient", logo: `${siteUrl}/favicon-light.svg` @@ -143,12 +142,11 @@ export function getBriefOGData( article: { publishedTime: brief.data.date, modifiedTime: brief.data.modifiedDate, - author: "Paul R. Berman", + author: "plx", section: category?.displayName || "Briefs" }, twitter: { - card: "summary_large_image", - creator: "@plxgithub" + card: "summary_large_image" } }; } @@ -169,7 +167,7 @@ export function getProjectOGData( ogImage = generateTailgraphURL({ title: project.data.title, subtitle: "Project", - author: "Paul R. Berman", + author: "plx", theme: "dark", backgroundImage: "gradient", logo: `${siteUrl}/favicon-light.svg` @@ -185,8 +183,7 @@ export function getProjectOGData( image: ogImage, imageAlt: project.data.ogImageAlt || `${ogTitle} - Project`, twitter: { - card: "summary_large_image", - creator: "@plxgithub" + card: "summary_large_image" } }; } @@ -221,8 +218,7 @@ export function getListOGData( image: ogImage, imageAlt: `${title} - ${SITE.NAME}`, twitter: { - card: "summary_large_image", - creator: "@plxgithub" + card: "summary_large_image" } }; } @@ -251,8 +247,7 @@ export function getHomeOGData( image: ogImage, imageAlt: SITE.NAME, twitter: { - card: "summary_large_image", - creator: "@plxgithub" + card: "summary_large_image" } }; -} \ No newline at end of file +}