Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 154 additions & 2 deletions .github/workflows/portfolio-project-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,31 @@ on:
required: true
default: false
type: boolean
deploy_preview:
description: "Deploy a preview after creating the project PR."
required: true
default: true
type: boolean
preview_repository:
description: "Preview hosting repository in owner/name format."
required: true
default: "vikramsh2002/My-Portfolio-Preview"
type: string
preview_branch:
description: "Branch used by GitHub Pages in the preview repository."
required: true
default: "gh-pages"
type: string
preview_base:
description: "Vite base path for the preview Pages site."
required: true
default: "/My-Portfolio-Preview/"
type: string
preview_url:
description: "Public preview URL."
required: true
default: "https://vikramsh2002.github.io/My-Portfolio-Preview/"
type: string
card_image_url:
description: "Optional image URL to use for the project card when no live site screenshot is available."
required: false
Expand All @@ -42,6 +67,11 @@ jobs:
UPDATE_TEXT: ${{ github.event.client_payload.linkedin_update || github.event.inputs.linkedin_update }}
MODEL_ID: ${{ github.event.client_payload.model || github.event.inputs.model || 'openai/gpt-4o' }}
AUTO_MERGE: ${{ github.event.client_payload.auto_merge || github.event.inputs.auto_merge || 'false' }}
DEPLOY_PREVIEW: ${{ github.event.client_payload.deploy_preview || github.event.inputs.deploy_preview || 'true' }}
PREVIEW_REPOSITORY: ${{ github.event.client_payload.preview_repository || github.event.inputs.preview_repository || 'vikramsh2002/My-Portfolio-Preview' }}
PREVIEW_BRANCH: ${{ github.event.client_payload.preview_branch || github.event.inputs.preview_branch || 'gh-pages' }}
PREVIEW_BASE: ${{ github.event.client_payload.preview_base || github.event.inputs.preview_base || '/My-Portfolio-Preview/' }}
PREVIEW_URL: ${{ github.event.client_payload.preview_url || github.event.inputs.preview_url || 'https://vikramsh2002.github.io/My-Portfolio-Preview/' }}
CARD_IMAGE_URL: ${{ github.event.client_payload.card_image_url || github.event.inputs.card_image_url }}
steps:
- name: Checkout
Expand Down Expand Up @@ -156,9 +186,121 @@ jobs:

echo "Triggered Portfolio V2 CI for $BRANCH_NAME." >> "$GITHUB_STEP_SUMMARY"

- name: Build preview
id: preview_build
if: steps.pr.outputs.pr_url != '' && env.DEPLOY_PREVIEW == 'true'
continue-on-error: true
working-directory: portfolio-v2
env:
VITE_BASE: ${{ env.PREVIEW_BASE }}
run: npm run build

- name: Smoke check preview artifact
id: preview_smoke
if: steps.pr.outputs.pr_url != '' && env.DEPLOY_PREVIEW == 'true' && steps.preview_build.outcome == 'success'
continue-on-error: true
env:
PROJECT_TITLE: ${{ steps.apply.outputs.project_title }}
run: |
if [ -z "$PROJECT_TITLE" ]; then
echo "No project title was available for preview smoke checking." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi

if grep -R --include='*.html' --include='*.js' --fixed-strings --quiet "$PROJECT_TITLE" portfolio-v2/dist; then
echo "Preview artifact contains project title: $PROJECT_TITLE" >> "$GITHUB_STEP_SUMMARY"
else
echo "Preview artifact does not contain expected project title: $PROJECT_TITLE" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi

- name: Deploy generated preview
id: preview_deploy
if: steps.pr.outputs.pr_url != '' && env.DEPLOY_PREVIEW == 'true' && steps.preview_build.outcome == 'success' && steps.preview_smoke.outcome == 'success'
continue-on-error: true
env:
PORTFOLIO_PREVIEW_TOKEN: ${{ secrets.PORTFOLIO_PREVIEW_TOKEN }}
SOURCE_REF: ${{ steps.pr.outputs.branch_name }}
run: |
set -euo pipefail

if [ -z "$PORTFOLIO_PREVIEW_TOKEN" ]; then
echo "PORTFOLIO_PREVIEW_TOKEN is required to push to $PREVIEW_REPOSITORY." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi

SOURCE_SHA="$(git rev-parse HEAD)"
PREVIEW_LINK="${PREVIEW_URL%/}/?v=${SOURCE_SHA}#projects"
PREVIEW_REMOTE="https://x-access-token:${PORTFOLIO_PREVIEW_TOKEN}@github.com/${PREVIEW_REPOSITORY}.git"

git clone --depth 1 --branch "$PREVIEW_BRANCH" "$PREVIEW_REMOTE" _preview || {
git clone --depth 1 "$PREVIEW_REMOTE" _preview
git -C _preview checkout --orphan "$PREVIEW_BRANCH"
git -C _preview rm -rf . || true
}

find _preview -mindepth 1 \
! -path "_preview/.git" \
! -path "_preview/.git/*" \
-exec rm -rf {} +

cp -R portfolio-v2/dist/. _preview/
touch _preview/.nojekyll

cat > _preview/preview-metadata.json <<EOF
{
"source_repository": "$GITHUB_REPOSITORY",
"source_ref": "$SOURCE_REF",
"source_sha": "$SOURCE_SHA",
"preview_base": "$PREVIEW_BASE",
"preview_url": "$PREVIEW_URL",
"deployed_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
EOF

git -C _preview config user.name "github-actions[bot]"
git -C _preview config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git -C _preview add .

if git -C _preview diff --cached --quiet; then
echo "Preview repository already matches this build." >> "$GITHUB_STEP_SUMMARY"
else
git -C _preview commit -m "Deploy preview for ${GITHUB_REPOSITORY}@${SOURCE_SHA::7}"
git -C _preview push origin "$PREVIEW_BRANCH"
fi

{
echo "## Portfolio Preview"
echo
echo "- Preview URL: $PREVIEW_LINK"
echo "- Preview repository: $PREVIEW_REPOSITORY"
echo "- Preview branch: $PREVIEW_BRANCH"
echo "- Source ref: $SOURCE_REF"
echo "- Source SHA: $SOURCE_SHA"
} >> "$GITHUB_STEP_SUMMARY"

echo "preview_link=$PREVIEW_LINK" >> "$GITHUB_OUTPUT"
echo "source_sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT"

- name: Comment preview link on PR
if: steps.preview_deploy.outputs.preview_link != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ steps.pr.outputs.pr_url }}
PREVIEW_LINK: ${{ steps.preview_deploy.outputs.preview_link }}
SOURCE_REF: ${{ steps.pr.outputs.branch_name }}
SOURCE_SHA: ${{ steps.preview_deploy.outputs.source_sha }}
run: |
PR_NUMBER="${PR_URL##*/}"
gh pr comment "$PR_NUMBER" --body "Portfolio preview deployed:

$PREVIEW_LINK

Source: $SOURCE_REF @ ${SOURCE_SHA:0:7}"

- name: Ask for review on blocked update
id: review_issue
if: always() && (steps.apply.outcome == 'failure' || steps.build.outcome == 'failure' || steps.apply.outputs.risk == 'high' || steps.apply.outputs.intent == 'needs_review' || steps.apply.outputs.action == 'needs_review' || steps.apply.outputs.image_status == 'missing' || steps.apply.outputs.image_status == 'failed')
if: always() && (steps.apply.outcome == 'failure' || steps.build.outcome == 'failure' || steps.apply.outputs.risk == 'high' || steps.apply.outputs.intent == 'needs_review' || steps.apply.outputs.action == 'needs_review' || steps.apply.outputs.image_status == 'missing' || steps.apply.outputs.image_status == 'failed' || (env.DEPLOY_PREVIEW == 'true' && (steps.preview_build.outcome == 'failure' || steps.preview_smoke.outcome == 'failure' || steps.preview_deploy.outcome == 'failure')))
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Expand All @@ -174,6 +316,10 @@ jobs:
echo "- Image status: ${{ steps.apply.outputs.image_status }}"
echo "- Image path: ${{ steps.apply.outputs.image_path }}"
echo "- Image source: ${{ steps.apply.outputs.image_source }}"
echo "- Preview build status: ${{ steps.preview_build.outcome }}"
echo "- Preview smoke status: ${{ steps.preview_smoke.outcome }}"
echo "- Preview deploy status: ${{ steps.preview_deploy.outcome }}"
echo "- Preview URL: ${{ steps.preview_deploy.outputs.preview_link }}"
echo "- Workflow run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
echo
if [ "${{ steps.apply.outputs.image_status }}" = "missing" ] || [ "${{ steps.apply.outputs.image_status }}" = "failed" ]; then
Expand Down Expand Up @@ -234,8 +380,14 @@ jobs:

echo "Review email sent to $ALERT_EMAIL." >> "$GITHUB_STEP_SUMMARY"

- name: Fail when preview is broken
if: always() && env.DEPLOY_PREVIEW == 'true' && (steps.preview_build.outcome == 'failure' || steps.preview_smoke.outcome == 'failure' || steps.preview_deploy.outcome == 'failure')
run: |
echo "Portfolio preview failed, so auto-merge is blocked." >> "$GITHUB_STEP_SUMMARY"
exit 1

- name: Auto-merge low-risk project update
if: env.AUTO_MERGE == 'true' && steps.apply.outputs.risk == 'low' && steps.pr.outputs.pr_url != ''
if: env.AUTO_MERGE == 'true' && steps.apply.outputs.risk == 'low' && steps.pr.outputs.pr_url != '' && (env.DEPLOY_PREVIEW != 'true' || (steps.preview_smoke.outcome == 'success' && steps.preview_deploy.outcome == 'success'))
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,24 @@ flowchart TD
This repository uses three safety layers:

- `Portfolio V2 CI`: builds `portfolio-v2` on pull requests without deploying.
- `Deploy Portfolio Preview`: manually builds a branch and publishes only the static `dist/` output to a separate preview repository.
- `Sync Portfolio Project with GitHub Models`: creates a project PR, dispatches CI, and can deploy the preview in one run.
- `Deploy Portfolio Preview`: manually rebuilds a branch preview when needed.
- `Deploy Portfolio V2 to GitHub Pages`: deploys the live site only from `main`.

Generated portfolio sync PRs are created by `github-actions[bot]`. Because GitHub does not fire normal `pull_request` workflows from `GITHUB_TOKEN`-created events, the sync workflow explicitly dispatches `Portfolio V2 CI` for the generated branch after it opens the PR.

```mermaid
flowchart TD
A["Feature branch"] --> B["Portfolio V2 CI"]
M["Generated project PR"] --> N["Sync workflow dispatches CI"]
S["Run AI project sync"] --> M["Generated project PR"]
M --> N["Sync workflow dispatches CI"]
N --> B
A["Feature branch"] --> B["Portfolio V2 CI"]
B --> C{"Build passes"}
C -->|No| D["Fix before merge"]
C -->|Yes| E["Optional manual preview deploy"]
C -->|Yes| E["Automatic or manual preview deploy"]
E --> F["Build with preview base path"]
F --> G["Push dist to preview repo"]
G --> H["Preview Pages URL"]
G --> H["Preview link on PR"]
H --> I{"Looks good"}
I -->|No| D
I -->|Yes| J["Merge to main"]
Expand All @@ -69,7 +71,9 @@ Source repo: vikramsh2002/My-Portfolio
Preview repo: vikramsh2002/My-Portfolio-Preview
```

The source code stays in `My-Portfolio`. The preview workflow builds the selected branch, then pushes only `portfolio-v2/dist` into the preview repository's Pages branch.
The source code stays in `My-Portfolio`. Preview deployment builds the selected branch, then pushes only `portfolio-v2/dist` into the preview repository's Pages branch.

The AI sync workflow can deploy preview automatically after it opens a PR. The manual preview workflow remains available when you want to rebuild a preview for an existing branch.

This keeps the preview URL separate from the live portfolio:

Expand Down
17 changes: 14 additions & 3 deletions portfolio-v2/docs/portfolio-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ Make sure GitHub Models is enabled for the repository first.
5. Paste the LinkedIn/profile update text. Include source, demo, live, or LinkedIn URLs if available.
6. Keep the default model or enter another GitHub Models id.
7. Optionally paste a `card_image_url` if the project does not have a live website that can be screenshotted.
8. Leave `auto_merge` off unless you want low-risk updates merged automatically.
8. Keep `deploy_preview` on so the generated PR gets a preview deployment automatically.
9. Leave `auto_merge` off unless you want low-risk updates merged automatically after CI and preview checks are safe.

## Automatic Trigger

Expand All @@ -32,6 +33,11 @@ curl --request POST \
"linkedin_update": "Built and shipped a new project...",
"model": "openai/gpt-4o",
"auto_merge": false,
"deploy_preview": true,
"preview_repository": "vikramsh2002/My-Portfolio-Preview",
"preview_branch": "gh-pages",
"preview_base": "/My-Portfolio-Preview/",
"preview_url": "https://vikramsh2002.github.io/My-Portfolio-Preview/",
"card_image_url": "https://example.com/project-card.png"
}
}'
Expand All @@ -45,13 +51,16 @@ The workflow:

- Sends the current projects and pasted update to GitHub Models.
- Converts the update into structured project data.
- Updates an existing project instead of creating a duplicate when the title, a similar title, or a project URL matches an existing card.
- Prepares a project thumbnail by reusing an existing local image, downloading `card_image_url`, or capturing a screenshot from the live/demo URL.
- Applies an add/update to the project list.
- Places newly added projects in the featured/top project row by default unless the update explicitly says not to feature them.
- Runs the Vite build.
- Opens a pull request with a summary when the update is safe enough for review.
- Dispatches `Portfolio V2 CI` for the generated PR branch so required checks run for bot-created PRs.
- Optionally enables auto-merge for low-risk updates after the build passes.
- Builds and deploys the generated branch to the preview repository when `deploy_preview` is enabled.
- Comments the preview link on the generated PR.
- Optionally enables auto-merge for low-risk updates after CI and preview deployment are safe.
- Creates a GitHub issue when the update is high-risk, unclear, missing a usable project image, or the build fails.
- Sends a review email when email secrets are configured.

Expand All @@ -73,7 +82,9 @@ The workflow treats dependency, workflow, secret, layout, and broad code changes

## Preview Before Merge

For generated project PRs, use the `Deploy Portfolio Preview` workflow before merging when you want to see the exact rendered UI on a separate Pages URL.
Generated project PRs can deploy preview automatically from the sync workflow. Keep `deploy_preview=true` for the normal one-click path.

You can still use the manual `Deploy Portfolio Preview` workflow when you want to rebuild a preview for an existing branch without rerunning AI sync.

The preview workflow:

Expand Down
Loading
Loading