first commit
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled

This commit is contained in:
2026-04-22 19:51:20 +07:00
commit 93d1b7c3d3
579 changed files with 99797 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
name: CI (Release)
on:
push:
tags:
- 'v[0-9]*'
# Tags must never be cancelled — each is a public release
concurrency:
group: ci-release-${{ github.ref_name }}
cancel-in-progress: false
# Workflow-level permissions set the ceiling for the reusable ci.yml.
# id-token is never in the default token, so it must be granted explicitly
# here — otherwise the ci: job's `permissions:` block exceeds the caller
# workflow's permissions and GitHub rejects the run with startup_failure.
permissions:
actions: read
contents: write
packages: write
id-token: write
jobs:
ci:
uses: ./.github/workflows/ci.yml
secrets: inherit
permissions:
actions: read
contents: write
packages: write
id-token: write
File diff suppressed because it is too large Load Diff
+158
View File
@@ -0,0 +1,158 @@
name: Cleanup GHCR Images
on:
schedule:
- cron: "30 4 * * *" # Daily at 04:30 UTC
workflow_dispatch:
inputs:
dry_run:
description: "Log what would be deleted without making changes"
required: false
default: true
type: boolean
retention_days:
description: "Delete versions older than this many days"
required: false
default: 14
type: number
min_keep:
description: "Always keep at least this many versions per package"
required: false
default: 10
type: number
permissions:
packages: write
env:
ORG: TryGhost
RETENTION_DAYS: ${{ inputs.retention_days || 14 }}
MIN_KEEP: ${{ inputs.min_keep || 10 }}
jobs:
cleanup:
name: Cleanup
runs-on: ubuntu-latest
strategy:
matrix:
package: [ghost, ghost-core, ghost-development]
steps:
- name: Delete old non-release versions
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ github.event_name == 'schedule' && 'false' || inputs.dry_run }}
PACKAGE: ${{ matrix.package }}
run: |
set -euo pipefail
cutoff=$(date -u -d "-${RETENTION_DAYS} days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
|| date -u -v-${RETENTION_DAYS}d +%Y-%m-%dT%H:%M:%SZ)
echo "Package: ${ORG}/${PACKAGE}"
echo "Cutoff: ${cutoff} (${RETENTION_DAYS} days ago)"
echo "Dry run: ${DRY_RUN}"
echo ""
# Pagination — collect all versions
page=1
all_versions="[]"
while true; do
if ! batch=$(gh api \
"/orgs/${ORG}/packages/container/${PACKAGE}/versions?per_page=100&page=${page}" \
--jq '.' 2>&1); then
if [ "$page" = "1" ]; then
echo "::error::API request failed: ${batch}"
exit 1
fi
echo "::warning::API request failed (page ${page}): ${batch}"
break
fi
count=$(echo "$batch" | jq 'length')
if [ "$count" = "0" ]; then
break
fi
all_versions=$(echo "$all_versions $batch" | jq -s 'add')
page=$((page + 1))
done
total=$(echo "$all_versions" | jq 'length')
echo "Total versions: ${total}"
# Classify versions
keep=0
delete=0
delete_ids=""
for row in $(echo "$all_versions" | jq -r '.[] | @base64'); do
_jq() { echo "$row" | base64 -d | jq -r "$1"; }
id=$(_jq '.id')
updated=$(_jq '.updated_at')
tags=$(_jq '[.metadata.container.tags[]] | join(",")')
# Keep versions with semver tags (v1.2.3, 1.2.3, 1.2)
if echo "$tags" | grep -qE '(^|,)v?[0-9]+\.[0-9]+\.[0-9]+(,|$)' || \
echo "$tags" | grep -qE '(^|,)[0-9]+\.[0-9]+(,|$)'; then
keep=$((keep + 1))
continue
fi
# Keep versions with 'latest' or 'main' or cache-main tags
if echo "$tags" | grep -qE '(^|,)(latest|main|cache-main)(,|$)'; then
keep=$((keep + 1))
continue
fi
# Keep versions newer than cutoff
if [[ "$updated" > "$cutoff" ]]; then
keep=$((keep + 1))
continue
fi
# This version is eligible for deletion
delete=$((delete + 1))
delete_ids="${delete_ids} ${id}"
tag_display="${tags:-<untagged>}"
if [ "$DRY_RUN" = "true" ]; then
echo "[dry-run] Would delete version ${id} (tags: ${tag_display}, updated: ${updated})"
fi
done
echo ""
echo "Summary: ${keep} kept, ${delete} to delete (of ${total} total)"
if [ "$delete" = "0" ]; then
echo "Nothing to delete."
exit 0
fi
# Safety check — run before dry-run exit so users see the warning
if [ "$keep" -lt "$MIN_KEEP" ]; then
echo "::error::Safety check failed — only ${keep} versions would remain (minimum: ${MIN_KEEP}). Aborting."
exit 1
fi
if [ "$DRY_RUN" = "true" ]; then
echo ""
echo "Dry run — no versions deleted."
exit 0
fi
# Delete eligible versions
deleted=0
failed=0
for id in $delete_ids; do
if gh api --method DELETE \
"/orgs/${ORG}/packages/container/${PACKAGE}/versions/${id}" 2>/dev/null; then
deleted=$((deleted + 1))
else
echo "::warning::Failed to delete version ${id}"
failed=$((failed + 1))
fi
done
echo ""
echo "Deleted ${deleted} versions (${failed} failed)"
+26
View File
@@ -0,0 +1,26 @@
name: "Copilot Setup Steps"
# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
# The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent
copilot-setup-steps:
runs-on: ubuntu-latest
# Set minimal permissions for setup steps
# Copilot Agent receives its own token with appropriate permissions
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install gh-aw extension
uses: github/gh-aw/actions/setup-cli@ce1794953e0ec42adc41b6fca05e02ab49ee21c3 # v0.68.3
with:
version: v0.49.3
@@ -0,0 +1,66 @@
name: Create release branch
on:
workflow_dispatch:
inputs:
base-ref:
description: 'Git ref to base from (defaults to latest tag)'
type: string
default: 'latest'
required: false
bump-type:
description: 'Version bump type (patch, minor)'
type: string
required: false
default: 'patch'
env:
FORCE_COLOR: 1
permissions:
contents: write
jobs:
create-branch:
if: github.repository == 'TryGhost/Ghost'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
if: inputs.base-ref == 'latest'
with:
ref: main
fetch-depth: 0
submodules: true
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
if: inputs.base-ref != 'latest'
with:
ref: ${{ inputs.base-ref }}
fetch-depth: 0
submodules: true
- name: Checkout most recent tag
run: git checkout "$(git describe --tags --abbrev=0 --match=v*)"
if: inputs.base-ref == 'latest'
- uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4
with:
tool_versions: |
semver 3.3.0
- run: |
CURRENT_TAG=$(git describe --tags --abbrev=0 --match=v*)
NEW_VERSION=$(semver bump "$BUMP_TYPE_INPUT" "$CURRENT_TAG")
printf 'CURRENT_SHA=%s\n' "$(git rev-parse HEAD)" >> "$GITHUB_ENV"
printf 'NEW_VERSION=%s\n' "$NEW_VERSION" >> "$GITHUB_ENV"
env:
BUMP_TYPE_INPUT: ${{ inputs.bump-type }}
- name: Create branch
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const branchName = `v${process.env.NEW_VERSION}`;
console.log(`Creating branch: ${branchName}`);
await github.request('POST /repos/{owner}/{repo}/git/refs', {
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/heads/${branchName}`,
sha: process.env.CURRENT_SHA
});
+127
View File
@@ -0,0 +1,127 @@
name: Deploy to Staging
# DISABLED: The deploy-to-staging label workflow is currently broken and disabled.
# Problems:
# 1. Admin is global — deploying a PR's admin overwrites admin-forward/ for ALL staging
# sites, not just demo.ghost.is. Per-site admin versioning is needed first.
# 2. Main merges overwrite — any merge to main triggers a full staging rollout that
# overwrites both the server version on demo.ghost.is and admin-forward/ globally.
# The deployment lasts only until the next merge to main, making it unreliable.
# See: https://www.notion.so/ghost/Proposal-Per-site-admin-versioning-31951439c03081daa133eb0215642202
on:
pull_request_target:
types: [labeled]
jobs:
deploy:
name: Deploy to Staging
# Runs when the "deploy-to-staging" label is added — requires collaborator write access.
# Fork PRs are rejected because they don't have GHCR images (CI uses artifact transfer).
if: >-
false
&& github.event.label.name == 'deploy-to-staging'
&& github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
steps:
- name: Wait for CI build artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Waiting for CI to complete Docker build for $HEAD_SHA..."
TIMEOUT=1800 # 30 minutes
INTERVAL=30
START=$(date +%s)
while true; do
ELAPSED=$(( $(date +%s) - START ))
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "::error::Timed out waiting for CI (${TIMEOUT}s)"
exit 1
fi
# Find the CI run for this SHA
RUN=$(gh api "repos/${{ github.repository }}/actions/workflows/ci.yml/runs?head_sha=${HEAD_SHA}&per_page=1" \
--jq '.workflow_runs[0] | {id, status, conclusion}' 2>/dev/null || echo "")
if [ -z "$RUN" ] || [ "$RUN" = "null" ]; then
echo " No CI run found yet, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)"
sleep "$INTERVAL"
continue
fi
STATUS=$(echo "$RUN" | jq -r '.status')
CONCLUSION=$(echo "$RUN" | jq -r '.conclusion // empty')
RUN_ID=$(echo "$RUN" | jq -r '.id')
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ] || [ "$CONCLUSION" = "failure" ]; then
# Check if Docker build job specifically succeeded (paginate — CI has 30+ jobs)
BUILD_JOB=$(gh api --paginate "repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs?per_page=100" \
--jq '.jobs[] | select(.name == "Build & Publish Artifacts") | .conclusion')
if [ -z "$BUILD_JOB" ]; then
echo "::error::Build & Publish Artifacts job not found in CI run ${RUN_ID}"
exit 1
elif [ "$BUILD_JOB" = "success" ]; then
echo "Docker build ready (CI run $RUN_ID)"
break
else
echo "::error::Docker build job did not succeed (conclusion: $BUILD_JOB)"
exit 1
fi
else
echo "::error::CI run failed (conclusion: $CONCLUSION)"
exit 1
fi
fi
echo " CI still running ($STATUS), waiting ${INTERVAL}s... (${ELAPSED}s elapsed)"
sleep "$INTERVAL"
done
- name: Re-check PR eligibility
id: recheck
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR=$(gh api "repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}" \
--jq '{state, labels: [.labels[].name], head_sha: .head.sha}')
STATE=$(echo "$PR" | jq -r '.state')
HAS_LABEL=$(echo "$PR" | jq '.labels | any(. == "deploy-to-staging")')
CURRENT_SHA=$(echo "$PR" | jq -r '.head_sha')
if [ "$STATE" != "open" ]; then
echo "::warning::PR is no longer open ($STATE), skipping dispatch"
echo "skip=true" >> "$GITHUB_OUTPUT"
elif [ "$HAS_LABEL" != "true" ]; then
echo "::warning::deploy-to-staging label was removed, skipping dispatch"
echo "skip=true" >> "$GITHUB_OUTPUT"
elif [ "$CURRENT_SHA" != "$HEAD_SHA" ]; then
echo "::warning::HEAD SHA changed ($HEAD_SHA → $CURRENT_SHA), skipping dispatch (new push will trigger CI)"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "PR still eligible for deploy"
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Dispatch to Ghost-Moya
if: steps.recheck.outputs.skip != 'true'
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
token: ${{ secrets.CANARY_DOCKER_BUILD }}
repository: TryGhost/Ghost-Moya
event-type: ghost-artifacts-ready
client-payload: >-
{
"ref": "${{ env.PR_NUMBER }}",
"source_repo": "${{ github.repository }}",
"pr_number": "${{ env.PR_NUMBER }}",
"deploy": "true"
}
+21
View File
@@ -0,0 +1,21 @@
name: 'Label Issues & PRs'
on:
workflow_dispatch:
issues:
types: [opened, closed, labeled]
pull_request_target:
types: [opened, closed, labeled]
schedule:
- cron: '0 * * * *'
permissions:
issues: write
pull-requests: write
jobs:
action:
runs-on: ubuntu-latest
if: github.repository_owner == 'TryGhost'
steps:
- uses: tryghost/actions/actions/label-actions@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main
File diff suppressed because it is too large Load Diff
+232
View File
@@ -0,0 +1,232 @@
---
description: Triage new Linear issues for the Berlin Bureau (BER) team — classify type, assign priority, tag product area, and post reasoning comments.
on:
workflow_dispatch:
schedule: daily on weekdays
permissions:
contents: read
if: github.repository == 'TryGhost/Ghost'
tools:
cache-memory: true
mcp-servers:
linear:
command: "npx"
args: ["-y", "mcp-remote", "https://mcp.linear.app/mcp", "--header", "Authorization:Bearer ${{ secrets.LINEAR_API_KEY }}"]
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
network:
allowed:
- defaults
- node
- mcp.linear.app
safe-outputs:
create-issue:
noop:
---
# Linear Issue Triage Agent
You are an AI agent that triages new Linear issues for the **Berlin Bureau (BER)** team. Your goal is to reduce the time a human needs to complete triage by pre-classifying issues, assigning priority, tagging product areas, and recommending code investigations where appropriate.
**You do not move issues out of Triage** — a human still makes the final call on status transitions.
## Your Task
1. Use the Linear MCP tools to find the BER team and list all issues currently in the **Triage** state
2. Check your cache-memory to see which issues you have already triaged — skip those
3. For each untriaged issue, apply the triage rubric below to:
- Classify the issue type
- Assign priority (both a priority label and Linear's built-in priority field)
- Tag the product area
- Post a triage comment explaining your reasoning
4. Update your cache-memory with the newly triaged issue IDs
5. After processing, call the `noop` safe output with a summary of what you did — e.g. "Triaged 1 issue: BER-3367 (Bug, P3)" or "No new BER issues in Triage state" if there was nothing to triage
## Linear MCP Tools
You have access to the official Linear MCP server. Use its tools to:
- **Find issues**: Search for BER team issues in Triage state
- **Read issue details**: Get title, description, labels, priority, and comments
- **Update issues**: Add labels and set priority
- **Create comments**: Post triage reasoning comments
Start by listing available tools to discover the exact tool names and parameters.
**Important:** When updating labels, preserve existing labels. Fetch the issue's current labels first, then include both old and new label IDs in the update.
## Cache-Memory Format
Store and read a JSON file at the **exact path** `cache-memory/triage-cache.json`. Always use this filename — never rename it or create alternative files.
```json
{
"triaged_issue_ids": ["BER-3150", "BER-3151"],
"last_run": "2025-01-15T10:00:00Z"
}
```
On each run:
1. Read `cache-memory/triage-cache.json` to get previously triaged issue identifiers
2. Skip any issues already in the list
3. After processing, write the updated list back to `cache-memory/triage-cache.json` (append newly triaged IDs)
## Triage Rubric
### Decision 1: Type Classification
Classify each issue based on its title, description, and linked context:
| Type | Signal words / patterns | Label to apply |
|------|------------------------|----------------|
| **Bug** | "broken", "doesn't work", "regression", "error", "crash", stack traces, Sentry links, "unexpected behaviour" | `🐛 Bug` (`e51776f7-038e-474b-86ec-66981c9abb4f`) |
| **Security** | "vulnerability", "exploit", "bypass", "SSRF", "XSS", "injection", "authentication bypass", "2FA", CVE references | `🔒 Security` (`28c5afc1-8063-4e62-af11-e42d94591957`) — also apply Bug if applicable |
| **Feature** | "add support for", "it would be nice", "can we", "new feature", Featurebase links | `✨ Feature` (`db8672e2-1053-4bc7-9aab-9d38c5b01560`) |
| **Improvement** | "improve", "enhance", "optimise", "refactor", "clean up", "polish" | `🎨 Improvement` (`b36579e6-62e1-4f55-987d-ee1e5c0cde1a`) |
| **Performance** | "slow", "latency", "timeout", "memory", "CPU", "performance", load time complaints | `⚡️ Performance` (`9066d0ea-6326-4b22-b6f5-82fe7ce2c1d1`) |
| **Maintenance** | "upgrade dependency", "tech debt", "remove deprecated", "migrate" | `🛠️ Maintenance` (`0ca27922-3646-4ab7-bf03-e67230c0c39e`) |
| **Documentation** | "docs", "README", "guide", "tutorial", missing documentation | `📝 Documentation` (`25f8988a-5925-44cd-b0df-c0229463925f`) |
If an issue matches multiple types (e.g. a security bug), apply all relevant labels.
### Decision 2: Priority Assignment
Assign priority to all issue types. Set both the Linear priority field and the corresponding priority label.
**For bugs and security issues**, use these criteria:
#### P1 — Urgent (Linear priority: 1, Label: `📊 Priority → P1 - Urgent` `11de115f-3e40-46c6-bf42-2aa2b9195cbd`)
- Security vulnerability with a clear exploit path
- Data loss or corruption (MySQL, disk) — actual or imminent (exception: small lexical data issues can be P2)
- Multiple customers' businesses immediately affected (broken payment collection, broken emails, broken member login)
#### P2 — High (Linear priority: 2, Label: `📊 Priority → P2 - High` `aeda47fa-9db9-4f4d-a446-3cccf92c8d12`)
- Triggering monitoring alerts that wake on-call engineers (if recurring, bump to P1)
- Security vulnerability without a clear exploit
- Regression that breaks currently working core functionality
- Crashes the server or browser
- Significantly disrupts customers' members/end-users (e.g. incorrect pricing or access)
- Bugs with members, subscriptions, or newsletters without immediate business impact
#### P3 — Medium (Linear priority: 3, Label: `📊 Priority → P3 - Medium` `10ec8b7b-725f-453f-b5d2-ff160d3b3c1e`)
- Bugs with members, subscriptions, or newsletters affecting only a few customers
- Bugs in recently released features that significantly affect usability
- Issues with setup/upgrade flows
- Broken features (dashboards, line charts, analytics, etc.)
- Correctness issues (e.g. timezones)
#### P4 — Low (Linear priority: 4, Label: `📊 Priority → P4 - Low` `411a21ea-c8c0-4cb1-9736-7417383620ff`)
- Not quite working as expected, but little overall impact
- Not related to payments, email, or security
- Significantly more complex to fix than the value of fixing
- Purely cosmetic
- Has a clear and straightforward workaround
**For non-bug issues** (features, improvements, performance, maintenance, documentation), assign a **provisional priority** based on estimated impact and urgency. Clearly mark it as provisional in the triage comment.
#### Bump Modifiers
**Bump UP one level if:**
- It causes regular alerts for on-call engineers
- It affects lots of users or VIP customers
- It prevents users from carrying out a critical use case or workflow
- It prevents rolling back to a previous release
**Bump DOWN one level if:**
- Reported by a single, non-VIP user
- Only impacts an edge case or obscure use case
Note in your comment if a bump modifier was applied and why.
### Decision 3: Product Area Tagging
Apply the most relevant `Product Area →` label:
| Label | Covers |
|-------|--------|
| `Product Area → Editor` | Post/page editor, Koenig, Lexical, content blocks |
| `Product Area → Dashboard` | Admin dashboard, stats, overview |
| `Product Area → Analytics` | Analytics, charts, reporting |
| `Product Area → Memberships` | Member management, segmentation, member data |
| `Product Area → Portal` | Member-facing portal, signup/login flows |
| `Product Area → Newsletters` | Email newsletters, sending, email design |
| `Product Area → Admin` | General admin UI, settings, navigation |
| `Product Area → Settings area` | Settings screens specifically |
| `Product Area → Billing App` | Billing, subscription management |
| `Product Area → Themes` | Theme system, Handlebars, theme marketplace |
| `Product Area → Publishing` | Post publishing, scheduling, distribution |
| `Product Area → Growth` | Growth features, recommendations |
| `Product Area → Comments` | Comment system |
| `Product Area → Imports / Exports` | Data import/export |
| `Product Area → Welcome emails / Automations` | Automated emails, welcome sequences |
| `Product Area → Social Web` | ActivityPub, federation |
| `Product Area → i18n` | Internationalisation, translations |
| `Product Area → Sodo Search` | Search functionality |
| `Product Area → Admin-X Offers` | Offers system in Admin-X |
If the issue spans multiple areas, apply all relevant labels. If no product area is clearly identifiable, don't force a label — note this in the comment.
**Important:** Use the Linear MCP tools to look up product area label IDs before applying them.
### Decision 4: Triage Comment
Post a comment on the issue with your reasoning. Use this format:
```
🤖 **Automated Triage**
**Type:** Bug (Security)
**Priority:** P2 — High
**Product Area:** Memberships
**Bump modifiers applied:** UP — affects multiple customers
**Reasoning:**
This appears to be a security vulnerability in the session handling that could allow
2FA bypass. While no clear exploit path has been reported, the potential for
authentication bypass affecting all staff accounts warrants P2. Bumped up from P3
because it affects all customers with 2FA enabled.
**Recommended action:** Code investigation recommended — this is a security bug
that needs code-level analysis.
```
For non-bug issues, mark priority as provisional:
```
🤖 **Automated Triage**
**Type:** Improvement
**Priority:** P3 — Medium *(provisional)*
**Product Area:** Admin
**Bump modifiers applied:** None
**Reasoning:**
This is a refactoring task to share logic between two related functions. No user-facing
impact, but reduces maintenance burden for the retention offers codebase. Provisional
P3 based on moderate codebase impact and alignment with active project work.
**Recommended action:** Code investigation recommended — small refactoring task with
clear scope, no design input needed.
```
### Decision 5: Code Investigation Recommendation
Flag an issue for code investigation in your comment if **all** of these are true:
1. Classified as a bug, security issue, performance issue, or small improvement/maintenance task
2. Does not require design input (no UI mockups needed, no UX decisions)
3. Has enough description to investigate (not just a title with no context)
Do **not** recommend investigation for:
- Feature requests (need product/design input)
- Issues with vague descriptions and no reproduction steps — instead note "Needs more info" in the comment
- Issues that are clearly large architectural changes
## Guidelines
- Process issues one at a time, applying all decisions before moving to the next
- Be concise but include enough reasoning that a human can quickly validate or override
- When in doubt about classification, pick the closest match and note your uncertainty
- If an issue already has triage labels or a triage comment from a previous run, skip it
- Never move issues out of the Triage state
- After processing all issues, update cache-memory with the full list of triaged identifiers
+57
View File
@@ -0,0 +1,57 @@
name: Migration Review
on:
pull_request_target:
types: [opened]
paths:
- 'ghost/core/core/server/data/schema/**'
- 'ghost/core/core/server/data/migrations/versions/**'
jobs:
createComment:
runs-on: ubuntu-latest
if: github.repository_owner == 'TryGhost'
name: Add migration review requirements
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["migration"]
})
- uses: peter-evans/create-or-update-comment@57232238742e38b2ccc27136ce596ccae7ca28b4
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
It looks like this PR contains a migration 👀
Here's the checklist for reviewing migrations:
### General requirements
- [ ] :warning: Tested performance on staging database servers, as performance on local machines is not comparable to a production environment
- [ ] Satisfies idempotency requirement (both `up()` and `down()`)
- [ ] Does not reference models
- [ ] Filename is in the correct format (and correctly ordered)
- [ ] Targets the next minor version
- [ ] All code paths have appropriate log messages
- [ ] Uses the correct utils
- [ ] Contains a minimal changeset
- [ ] Does not mix DDL/DML operations
- [ ] Tested in MySQL and SQLite
### Schema changes
- [ ] Both schema change and related migration have been implemented
- [ ] For index changes: has been performance tested for large tables
- [ ] For new tables/columns: fields use the appropriate predefined field lengths
- [ ] For new tables/columns: field names follow the appropriate conventions
- [ ] Does not drop a non-alpha table outside of a major version
### Data changes
- [ ] Mass updates/inserts are batched appropriately
- [ ] Does not loop over large tables/datasets
- [ ] Defends against missing or invalid data
- [ ] For settings updates: follows the appropriate guidelines
+137
View File
@@ -0,0 +1,137 @@
name: PR Preview
on:
pull_request_target:
types: [labeled, unlabeled, closed]
jobs:
deploy:
name: Deploy Preview
# Runs when the "preview" label is added — requires collaborator write access
if: >-
github.event.action == 'labeled'
&& github.event.label.name == 'preview'
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
steps:
- name: Wait for Docker build job
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_JOB_NAME: Build & Publish Artifacts
run: |
echo "Waiting for '${BUILD_JOB_NAME}' job to complete for $HEAD_SHA..."
TIMEOUT=1800 # 30 minutes
INTERVAL=30
START=$(date +%s)
while true; do
ELAPSED=$(( $(date +%s) - START ))
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "::error::Timed out waiting for '${BUILD_JOB_NAME}' (${TIMEOUT}s)"
exit 1
fi
# Find the CI run for this SHA
RUN=$(gh api "repos/${{ github.repository }}/actions/workflows/ci.yml/runs?head_sha=${HEAD_SHA}&per_page=1" \
--jq '.workflow_runs[0] | {id, status}' 2>/dev/null || echo "")
if [ -z "$RUN" ] || [ "$RUN" = "null" ]; then
echo " No CI run found yet, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)"
sleep "$INTERVAL"
continue
fi
RUN_ID=$(echo "$RUN" | jq -r '.id')
RUN_STATUS=$(echo "$RUN" | jq -r '.status')
# Look up the build job specifically (paginate — CI has 30+ jobs)
BUILD_JOB=$(gh api --paginate "repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs?per_page=100" \
--jq ".jobs[] | select(.name == \"${BUILD_JOB_NAME}\") | {status, conclusion}")
if [ -z "$BUILD_JOB" ]; then
if [ "$RUN_STATUS" = "completed" ]; then
echo "::error::CI run ${RUN_ID} completed but '${BUILD_JOB_NAME}' job was not found"
exit 1
fi
echo " '${BUILD_JOB_NAME}' job not started yet (run ${RUN_STATUS}), waiting ${INTERVAL}s... (${ELAPSED}s elapsed)"
sleep "$INTERVAL"
continue
fi
JOB_STATUS=$(echo "$BUILD_JOB" | jq -r '.status')
JOB_CONCLUSION=$(echo "$BUILD_JOB" | jq -r '.conclusion // empty')
if [ "$JOB_STATUS" = "completed" ]; then
if [ "$JOB_CONCLUSION" = "success" ]; then
echo "Docker build ready (CI run $RUN_ID)"
break
fi
echo "::error::'${BUILD_JOB_NAME}' did not succeed (conclusion: $JOB_CONCLUSION)"
exit 1
fi
echo " '${BUILD_JOB_NAME}' still ${JOB_STATUS}, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)"
sleep "$INTERVAL"
done
- name: Re-check PR eligibility
id: recheck
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" \
--jq '{state, labels: [.labels[].name]}')
STATE=$(echo "$PR" | jq -r '.state')
HAS_LABEL=$(echo "$PR" | jq '.labels | any(. == "preview")')
if [ "$STATE" != "open" ]; then
echo "::warning::PR is no longer open ($STATE), skipping dispatch"
echo "skip=true" >> "$GITHUB_OUTPUT"
elif [ "$HAS_LABEL" != "true" ]; then
echo "::warning::preview label was removed, skipping dispatch"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "PR still eligible for preview deploy"
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Dispatch deploy to Ghost-Moya
if: steps.recheck.outputs.skip != 'true'
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
token: ${{ secrets.CANARY_DOCKER_BUILD }}
repository: TryGhost/Ghost-Moya
event-type: preview-deploy
client-payload: >-
{
"pr_number": "${{ github.event.pull_request.number }}",
"action": "deploy",
"seed": "true"
}
destroy:
name: Destroy Preview
# Runs when "preview" label is removed, or the PR is closed/merged while labeled
if: >-
(github.event.action == 'unlabeled' && github.event.label.name == 'preview')
|| (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview'))
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Dispatch destroy to Ghost-Moya
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
token: ${{ secrets.CANARY_DOCKER_BUILD }}
repository: TryGhost/Ghost-Moya
event-type: preview-destroy
client-payload: >-
{
"pr_number": "${{ github.event.pull_request.number }}",
"action": "destroy"
}
+46
View File
@@ -0,0 +1,46 @@
name: Publish tb-cli Image
on:
workflow_dispatch: # Manual trigger from GitHub UI or CLI
push:
branches: [main]
paths:
- 'docker/tb-cli/**'
permissions:
contents: read
packages: write
jobs:
publish:
name: Build and push tb-cli to GHCR
runs-on: ubuntu-latest
if: github.repository == 'TryGhost/Ghost' && github.ref == 'refs/heads/main'
concurrency:
group: publish-tb-cli
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
context: .
file: docker/tb-cli/Dockerfile
push: true
tags: |
ghcr.io/tryghost/tb-cli:latest
ghcr.io/tryghost/tb-cli:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
+112
View File
@@ -0,0 +1,112 @@
name: Release
run-name: "Release — ${{ inputs.bump-type || 'auto' }} from ${{ inputs.branch || 'main' }}${{ inputs.dry-run && ' (dry run)' || '' }}"
on:
schedule:
- cron: '0 15 * * 5' # Friday 3pm UTC
workflow_dispatch:
inputs:
branch:
description: 'Git branch to release from'
type: string
default: 'main'
required: false
bump-type:
description: 'Version bump type (auto, patch, minor)'
type: string
required: false
default: 'auto'
skip-checks:
description: 'Skip CI status check verification'
type: boolean
default: false
dry-run:
description: 'Dry run (version bump without push)'
type: boolean
default: false
env:
FORCE_COLOR: 1
NODE_VERSION: 22.18.0
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
name: Prepare & Push Release
steps:
- uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
with:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Deploy key (via ssh-agent) is used for git push — it bypasses
# branch protection and triggers downstream workflows (unlike GITHUB_TOKEN)
ref: ${{ inputs.branch || 'main' }}
fetch-depth: 0
ssh-key: ${{ secrets.DEPLOY_KEY }}
# Fetch submodules separately via HTTPS — the deploy key is scoped to
# Ghost only and can't authenticate against Casper/Source over SSH
- run: git submodule update --init
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
env:
FORCE_COLOR: 0
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set up Git
run: |
git config user.name "Ghost CI"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Set up schedule defaults
if: github.event_name == 'schedule'
run: |
echo "RELEASE_BRANCH=main" >> "$GITHUB_ENV"
echo "RELEASE_BUMP_TYPE=auto" >> "$GITHUB_ENV"
echo "RELEASE_DRY_RUN=" >> "$GITHUB_ENV"
echo "RELEASE_SKIP_CHECKS=" >> "$GITHUB_ENV"
- name: Set up workflow_dispatch inputs
if: github.event_name == 'workflow_dispatch'
run: |
echo "RELEASE_BRANCH=${INPUT_BRANCH}" >> "$GITHUB_ENV"
echo "RELEASE_BUMP_TYPE=${INPUT_BUMP_TYPE}" >> "$GITHUB_ENV"
echo "RELEASE_DRY_RUN=${INPUT_DRY_RUN}" >> "$GITHUB_ENV"
echo "RELEASE_SKIP_CHECKS=${INPUT_SKIP_CHECKS}" >> "$GITHUB_ENV"
env:
INPUT_BRANCH: ${{ inputs.branch }}
INPUT_BUMP_TYPE: ${{ inputs.bump-type }}
INPUT_DRY_RUN: ${{ inputs.dry-run }}
INPUT_SKIP_CHECKS: ${{ inputs.skip-checks }}
- name: Run release script
run: |
ARGS="--branch=${{ env.RELEASE_BRANCH }} --bump-type=${{ env.RELEASE_BUMP_TYPE }}"
if [ "${{ env.RELEASE_DRY_RUN }}" = "true" ]; then
ARGS="$ARGS --dry-run"
fi
if [ "${{ env.RELEASE_SKIP_CHECKS }}" = "true" ]; then
ARGS="$ARGS --skip-checks"
fi
node scripts/release.js $ARGS
env:
GITHUB_TOKEN: ${{ secrets.CANARY_DOCKER_BUILD }} # PAT for GitHub API (check polling)
- name: Notify on failure
if: failure()
uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main
with:
status: ${{ job.status }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+26
View File
@@ -0,0 +1,26 @@
name: 'Close stale i18n PRs'
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * *'
jobs:
stale:
if: github.repository_owner == 'TryGhost'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
with:
stale-pr-message: |
Thanks for contributing to Ghost's i18n :)
This PR has been automatically marked as stale because there has not been any activity here in 3 weeks.
I18n PRs tend to get out of date quickly, so we're closing them to keep the PR list clean.
If you're still interested in working on this PR, please let us know. Otherwise this PR will be closed shortly, but can always be reopened later. Thank you for understanding 🙂
only-labels: 'affects:i18n'
days-before-pr-stale: 21
days-before-pr-close: 7
exempt-pr-labels: 'feature,pinned,needs:triage'
stale-pr-label: 'stale'
close-pr-message: |
This PR has been automatically closed due to inactivity. If you'd like to continue working on it, feel free to open a new PR.
+29
View File
@@ -0,0 +1,29 @@
name: 'Close stale issues and PRs'
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * *'
jobs:
stale:
if: github.repository_owner == 'TryGhost'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
with:
stale-issue-message: |
Our bot has automatically marked this issue as stale because there has not been any activity here in some time.
The issue will be closed soon if there are no further updates, however we ask that you do not post comments to keep the issue open if you are not actively working on a PR.
We keep the issue list minimal so we can keep focus on the most pressing issues. Closed issues can always be reopened if a new contributor is found. Thank you for understanding 🙂
stale-pr-message: |
Our bot has automatically marked this PR as stale because there has not been any activity here in some time.
If weve missed reviewing your PR & youre still interested in working on it, please let us know. Otherwise this PR will be closed shortly, but can always be reopened later. Thank you for understanding 🙂
exempt-issue-labels: 'feature,pinned,needs:triage'
exempt-pr-labels: 'feature,pinned,needs:triage'
days-before-stale: 113
days-before-pr-stale: 358
stale-issue-label: 'stale'
stale-pr-label: 'stale'
close-issue-reason: 'not_planned'