GitHub Actions Automation Reference

Overview

This document describes the GitHub Actions workflows that automate the content generation pipeline. Two primary workflows handle content scheduling and validation.

Workflows

1. Calendar Check (calendar-check.yml)

Purpose: Automatically scans editorial calendars and creates GitHub Issues for content due within the next 7 days.

Location: .github/workflows/calendar-check.yml

Triggers

on:
  schedule:
    # Run every Monday at 9am UTC (4am EST)
    - cron: '0 9 * * 1'
  workflow_dispatch:
    inputs:
      days_ahead:
        description: 'Days to look ahead for due content'
        required: false
        default: '7'
        type: string
      create_issues:
        description: 'Actually create issues (false = dry run)'
        required: false
        default: true
        type: boolean
Trigger When Notes
schedule Monday 9:00 UTC Automated weekly run
workflow_dispatch Manual For testing or ad-hoc runs

Inputs (Manual Trigger)

Input Type Default Description
days_ahead string "7" Number of days to look ahead
create_issues boolean true Set false for dry run

Permissions

permissions:
  contents: read    # Read calendar files
  issues: write     # Create issues

Workflow Steps

Step 1: Checkout Repository
- name: Checkout
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Step 2: Setup Node.js
- name: Setup Node.js
  uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
  with:
    node-version: '22'
Step 3: Install Dependencies
- name: Install dependencies
  run: npm install js-yaml
Step 4: Scan Calendars

The main logic reads all YAML files in the calendar/ directory:

function checkCalendarFile(filepath) {
  const content = fs.readFileSync(filepath, 'utf8');
  const cal = yaml.load(content);

  if (!cal.months) return;

  for (const [month, data] of Object.entries(cal.months)) {
    if (!data.posts) continue;

    for (const post of data.posts) {
      // Skip already published or in-progress items
      if (post.status === 'published' || post.status === 'in-progress') continue;

      // Check if due_date falls within the lookahead window
      if (post.due_date) {
        const dueDate = new Date(post.due_date);
        if (dueDate >= today && dueDate <= cutoff) {
          dueItems.push({
            ...post,
            month,
            source: filepath
          });
        }
      }
    }
  }
}
Step 5: Create Issues

For each due item, the workflow:

  1. Checks for Existing Issues
    gh issue list --search "${item.title} in:title" --json number --limit 1
    
  2. Skips Duplicates
    if (existing.length > 0) {
      console.log(`Issue already exists for: ${item.title} (#${existing[0].number})`);
      continue;
    }
    
  3. Creates New Issue
    gh issue create --title "[${type}] ${title}" --body "${body}" --label "${labels}"
    
Step 6: Generate Summary
- name: Summary
  run: |
    echo "## Calendar Check Summary" >> $GITHUB_STEP_SUMMARY
    # ... summary generation

Issue Body Template

## Content Request from Editorial Calendar

**Type**: ${item.type}
**Title**: ${item.title}
**Due Date**: ${item.due_date}
**Publish Date**: ${item.publish_date || 'TBD'}
**Priority**: ${item.priority || 'medium'}

### Description
${item.description || 'No description provided'}

### Topics
${(item.topics || []).map(t => `- ${t}`).join('\n') || 'None specified'}

### Platforms
${(item.platforms || []).map(p => `- ${p}`).join('\n') || 'Blog only'}

---

### For Copilot

Use the following resources:
- Brand voice: `brands/zircote/brand.yml`
- Blog template: `brands/zircote/templates/blog-post.prompt.md`
- Schema: `schemas/blog.schema.yml`

Save draft to: `content/blog/drafts/${due_date}-${slug}.md`

---
*Auto-generated from calendar/${source}*

Labels Applied

Label When Applied
content Always
copilot Always (for GitHub Copilot visibility)
blog When type: blog
social When type: social
video When type: video
priority When priority: high or critical

2. Content Validation (validate-content.yml)

Purpose: Validates content structure and formatting on pull requests.

Location: .github/workflows/validate-content.yml

Triggers

on:
  pull_request:
    paths:
      - 'content/**'
      - '_posts/**'

Validation Checks

  1. Frontmatter Validation
    • Required fields present
    • Date format (YYYY-MM-DD)
    • Valid categories
  2. Schema Compliance
    • Blog posts match schemas/blog.schema.yml
    • Social posts match schemas/social.schema.yml
  3. Link Validation
    • Internal links resolve
    • External links accessible

Manual Workflow Execution

Running Calendar Check Manually

# Default (7 days, create issues)
gh workflow run calendar-check.yml

# Custom lookahead
gh workflow run calendar-check.yml -f days_ahead=14

# Dry run (no issues created)
gh workflow run calendar-check.yml -f create_issues=false

# Combined
gh workflow run calendar-check.yml -f days_ahead=14 -f create_issues=false

Viewing Workflow Runs

# List recent runs
gh run list --workflow=calendar-check.yml

# View specific run
gh run view <run-id>

# View run logs
gh run view <run-id> --log

Viewing Created Issues

# List content issues
gh issue list --label content

# List by type
gh issue list --label blog
gh issue list --label social

# List priority items
gh issue list --label priority

Cron Schedule Reference

GitHub Actions Cron Syntax

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of the month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday)
│ │ │ │ │
│ │ │ │ │
* * * * *

Current Schedule

- cron: '0 9 * * 1'
#        │ │ │ │ └─ Monday
#        │ │ │ └─── Every month
#        │ │ └───── Every day of month
#        │ └─────── 9:00 AM UTC
#        └───────── 0 minutes

Alternative Schedules

# Twice weekly (Monday and Thursday)
- cron: '0 9 * * 1,4'

# Daily at 8 AM UTC
- cron: '0 8 * * *'

# Every 6 hours
- cron: '0 */6 * * *'

# First Monday of month only
- cron: '0 9 1-7 * 1'

Timezone Considerations

GitHub Actions cron runs in UTC. Convert as needed:

UTC EST PST Notes
09:00 04:00 01:00 Current schedule
13:00 08:00 05:00 Morning EST
14:00 09:00 06:00 Morning PST

Secrets and Environment

Required Secrets

Secret Purpose How to Set
GITHUB_TOKEN Issue creation Automatic (provided by Actions)

Optional Secrets

Secret Purpose When Needed
SLACK_WEBHOOK Notifications If Slack integration added
DISCORD_WEBHOOK Notifications If Discord integration added

Error Handling

Common Errors

Calendar Parsing Errors

Error reading calendar/2026/q1.yml: Invalid YAML syntax

Fix: Validate YAML syntax with:

yq eval 'select(. != null)' calendar/2026/q1.yml

Issue Creation Failures

Failed to create issue for Title: error message

Causes:

Duplicate Issue Detection

Issue already exists for: Title (#123)

Normal behavior: Workflow skips creating duplicates.

Debugging

# View full workflow logs
gh run view <run-id> --log

# Download log artifacts
gh run download <run-id>

# Re-run failed workflow
gh run rerun <run-id>

Extending the Workflows

Adding Slack Notifications

- name: Notify Slack
  if: steps.check.outputs.count != '0'
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "📅 Content due this week: $ items"
      }
  env:
    SLACK_WEBHOOK_URL: $

Adding Email Notifications

- name: Send Email
  uses: dawidd6/action-send-mail@v3
  with:
    server_address: smtp.gmail.com
    server_port: 465
    username: $
    password: $
    subject: Content Due This Week
    to: you@example.com
    from: GitHub Actions
    body: "$ items due"

Custom Issue Assignees

Modify the issue creation to add assignees:

gh issue create \
  --title "[${type}] ${title}" \
  --body "${body}" \
  --label "${labels}" \
  --assignee "@me"

Priority-Based Assignees

const assignees = [];
if (item.priority === 'critical' || item.priority === 'high') {
  assignees.push('zircote');
}
if (item.type === 'video') {
  assignees.push('video-producer');
}

Monitoring

Workflow Status Badge

Add to README:

[![Calendar Check](https://github.com/zircote/zircote.github.io/actions/workflows/calendar-check.yml/badge.svg)](https://github.com/zircote/zircote.github.io/actions/workflows/calendar-check.yml)

Scheduled Run History

# View last 10 scheduled runs
gh run list --workflow=calendar-check.yml --limit=10 --event=schedule

Setting Up Alerts

Configure GitHub notification settings:

  1. Settings → Notifications → Actions
  2. Enable email for workflow failures
  3. Enable mobile push for critical workflows

Troubleshooting

Workflow Not Running

  1. Check schedule is correct
    gh workflow view calendar-check.yml
    
  2. Verify workflow is enabled
    • Actions tab → workflow → Enable if disabled
  3. Check for repository limits
    • Free tier: 2,000 minutes/month
    • If exceeded, wait for reset or upgrade

Issues Not Being Created

  1. Check permissions
    permissions:
      issues: write
    
  2. Check token has correct scopes
    • GITHUB_TOKEN should work automatically
  3. Verify calendar has due content
    # Dry run to check
    gh workflow run calendar-check.yml -f create_issues=false
    

Duplicate Issues Being Created

The workflow checks for duplicates by title. If duplicates appear:

  1. Issue title changed in calendar
    • Old title issue still exists
    • New title creates new issue
  2. Fix: Close duplicate issues manually
    gh issue close <number> --comment "Duplicate of #<other>"