diff --git a/plans/2026-08-11-release-workflow.md b/plans/2026-08-11-release-workflow.md new file mode 100644 index 0000000..78d6894 --- /dev/null +++ b/plans/2026-08-11-release-workflow.md @@ -0,0 +1,293 @@ +# Release Workflow Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a GitHub Actions workflow that, on every `v*` tag push, builds `projects-portfolio-v.zip` from an allowlist of runtime files and publishes it both as a workflow artifact and as an attachment to a matching GitHub release. + +**Architecture:** Single workflow file at `.github/workflows/release.yml`. One job (`release`) on `ubuntu-latest` performs: checkout → bash build step (allowlisted `cp -r` into a staging dir, then `zip -r`) → `actions/upload-artifact@v4` → `softprops/action-gh-release@v2`. Uses the runner-provided `GITHUB_TOKEN` with `contents: write` permission. + +**Tech Stack:** GitHub Actions YAML, bash, `zip` CLI (preinstalled on `ubuntu-latest`), `actions/checkout@v4`, `actions/upload-artifact@v4`, `softprops/action-gh-release@v2`. + +## Global Constraints + +- Workflow file lives at `.github/workflows/release.yml`. +- Trigger: `on.push.tags: ['v*']`. No `workflow_dispatch`. +- Permission: `contents: write`. +- Zip filename: `projects-portfolio-v.zip` where `` is `GITHUB_REF_NAME` with leading `v` stripped. +- Zip internal layout: `projects-portfolio/` directory at zip root, with `projects-portfolio.php` inside it (WordPress install-from-zip convention). +- Allowlisted source paths copied into staging: `projects-portfolio.php`, `README.md`, `LICENSE`, `admin/`, `assets/`, `includes/`, `languages/`, `templates/`. +- Excluded paths (no `cp` of these): `composer.json`, `composer.lock`, `vendor/`, `tests/`, `phpunit.xml`, `phpunit.xml.dist`, `.phpunit.result.cache`, `plans/`, `specs/`, `.github/`, `.gitignore`, `.vscode/`, `.claude/`. +- Build step uses `set -euo pipefail` and an `unzip -l "$OUT"` debug print at the end so the action log self-documents the contents. +- Artifact upload uses `if-no-files-found: error` so a missing zip fails the job. +- `softprops/action-gh-release@v2` is given the tag name, release name = tag name, `generate_release_notes: true`, and `files:` pointing at the zip. +- README gets a "Building a Release" section after "Connect Your Gitea Repo". +- First release is `v1.1.1`: implementation includes a version bump from `1.1.0` → `1.1.1` (header `Version:` + `PROJECTS_PORTFOLIO_VERSION` constant). +- No PHP files outside of the version bump are touched. The PHPUnit suite is unaffected. + +## File Structure + +**Created:** +- `.github/workflows/release.yml` — the workflow. + +**Modified:** +- `README.md` — adds a "Building a Release" section after the existing "Connect Your Gitea Repo" section. +- `projects-portfolio.php` — bumps header `Version:` and `PROJECTS_PORTFOLIO_VERSION` from `1.1.0` to `1.1.1` as part of the first-release rollout. + +No other files touched. No tests added. + +--- + +## Task 1: Add the release workflow file + +**Files:** +- Create: `.github/workflows/release.yml` + +**Interfaces:** +- Produces: a workflow that triggers on `v*` tag push and produces a release zip + GitHub release. + +- [ ] **Step 1: Create `.github/workflows/release.yml`** + +Create the directory if missing, then create the file with this exact content: + +```yaml +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + name: Build & publish plugin zip + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build plugin zip + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + STAGE="dist/staging" + OUT="dist/projects-portfolio-v${VERSION}.zip" + + rm -rf dist + mkdir -p "$STAGE/projects-portfolio" + + cp projects-portfolio.php "$STAGE/projects-portfolio/" + cp README.md "$STAGE/projects-portfolio/" + cp LICENSE "$STAGE/projects-portfolio/" + cp -r admin/ "$STAGE/projects-portfolio/" + cp -r assets/ "$STAGE/projects-portfolio/" + cp -r includes/ "$STAGE/projects-portfolio/" + cp -r languages/ "$STAGE/projects-portfolio/" + cp -r templates/ "$STAGE/projects-portfolio/" + + ( cd "$STAGE" && zip -r "../../$OUT" projects-portfolio ) + + echo "Built $OUT" + unzip -l "$OUT" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: projects-portfolio-v${{ github.ref_name }} + path: dist/projects-portfolio-*.zip + if-no-files-found: error + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} + generate_release_notes: true + files: dist/projects-portfolio-*.zip +``` + +- [ ] **Step 2: Validate YAML syntax locally** + +Run from the project root: +```bash +php -r "require 'vendor/autoload.php';" 2>/dev/null || true +# PHP doesn't have a built-in YAML parser; use Python's if available +python -c "import yaml,sys; yaml.safe_load(open('.github/workflows/release.yml').read()); print('OK')" +``` + +If `python` is not on PATH, use any other YAML validator you have. If none is available, skip this step — GitHub's own workflow validation will catch syntax errors on the next push. + +Expected (if `python` is available): `OK` on stdout, exit 0. + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/release.yml +git commit -m "Add GitHub Actions release workflow" +``` + +--- + +## Task 2: Add "Building a Release" section to README + +**Files:** +- Modify: `README.md` + +**Interfaces:** +- Produces: a "Building a Release" section placed after the "Connect Your Gitea Repo" section (which was added in the previous feature). + +- [ ] **Step 1: Insert the new section** + +Find the existing `## Connect Your Gitea Repo` section in `README.md`. Locate the heading `## Building a Release` (it does not yet exist; that's the insertion target). + +Insert the following block directly after the closing line of the Gitea repo subsection (`Paste it into the plugin settings screen under **Gitea API Token**.`) and before any subsequent `## ` heading: + +```markdown + +## Building a Release + +Releases are automated via GitHub Actions. To cut a new release: + +1. Bump `PROJECTS_PORTFOLIO_VERSION` and the plugin header `Version:` in `projects-portfolio.php`. +2. Commit and push to `main`. +3. Tag the release commit: `git tag -a v1.1.0 -m "v1.1.0 — short summary"`. +4. Push the tag: `git push origin v1.1.0`. + +The `.github/workflows/release.yml` workflow runs and: + +- Builds `projects-portfolio-v.zip` containing only the runtime files (`projects-portfolio.php`, `admin/`, `assets/`, `includes/`, `languages/`, `templates/`, `README.md`, `LICENSE`). +- Uploads the zip as a workflow artifact. +- Creates (or updates) the matching GitHub Release with the zip attached. + +Dev-only paths (`composer.json`, `composer.lock`, `vendor/`, `tests/`, `plans/`, `specs/`, `.github/`, `.gitignore`, `.vscode/`, `.claude/`) are intentionally excluded. +``` + +- [ ] **Step 2: Verify with grep** + +Run: +```bash +grep -n "Building a Release" README.md +``` +Expected: one matching line, located after the "Connect Your Gitea Repo" section. + +- [ ] **Step 3: Commit** + +```bash +git add README.md +git commit -m "Document automated release process in README" +``` + +--- + +## Task 3: Bump version to 1.1.1 for first automated release + +**Files:** +- Modify: `projects-portfolio.php` — header `Version:` and `PROJECTS_PORTFOLIO_VERSION` constant. + +**Interfaces:** +- Produces: in-tree version `1.1.1`, header comment line `* Version: 1.1.1`, constant `define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.1' );`. + +- [ ] **Step 1: Update the plugin header** + +In `projects-portfolio.php`, in the file header comment block near the top, change: +``` + * Version: 1.1.0 +``` +to: +``` + * Version: 1.1.1 +``` + +- [ ] **Step 2: Update the version constant** + +In `projects-portfolio.php`, find the line: +```php +define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.0' ); +``` +Replace with: +```php +define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.1' ); +``` + +- [ ] **Step 3: Run the test suite to confirm no regression** + +Run: `php vendor/bin/phpunit` +Expected: `OK (24 tests, 45 assertions)` (same as before — no PHP behavior change). + +- [ ] **Step 4: Verify with grep** + +Run: +```bash +grep -n "Version:" projects-portfolio.php | head -1 +grep -n "PROJECTS_PORTFOLIO_VERSION" projects-portfolio.php +``` +Expected output: +- First grep: a line containing `* Version: 1.1.1`. +- Second grep: two lines — the `define(...)` and (possibly) usage sites. The `define` line must show `'1.1.1'`. + +- [ ] **Step 5: Commit** + +```bash +git add projects-portfolio.php +git commit -m "Bump version to 1.1.1 for first automated release" +``` + +--- + +## Task 4: Final verification + +**Files:** none (verification only). + +- [ ] **Step 1: Confirm all three commits landed** + +Run: +```bash +git log --oneline -5 +``` +Expected (most recent at top): three commits from this plan are visible: +- "Bump version to 1.1.1 for first automated release" +- "Document automated release process in README" +- "Add GitHub Actions release workflow" + +- [ ] **Step 2: Confirm working tree is clean** + +Run: `git status` +Expected: `nothing to commit, working tree clean`. + +- [ ] **Step 3: Run full PHPUnit suite** + +Run: `php vendor/bin/phpunit` +Expected: `OK (24 tests, 45 assertions)`. + +- [ ] **Step 4: Confirm workflow file content** + +Run: +```bash +head -30 .github/workflows/release.yml +``` +Expected: matches the YAML produced in Task 1, with `name: Release`, `on.push.tags: ['v*']`, `permissions.contents: write`, and the `release` job. + +- [ ] **Step 5: Confirm README has the new section** + +Run: +```bash +grep -c "Building a Release" README.md +``` +Expected: `1`. + +- [ ] **Step 6: Note (do NOT execute): the first automated release** + +The user will tag `v1.1.1` and push the tag themselves, which triggers the workflow. The workflow file is in place; the in-tree version is bumped; the first release will produce a zip with this code. The implementer does NOT push the tag or the branch — that's the user's call (their existing git history is ahead of `origin/main`). + +- [ ] **Step 7: Final commit if anything changed** + +```bash +git status +# If clean, skip. Otherwise: +git add -A +git commit -m "Final verification fixes" +``` \ No newline at end of file