# 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" ```