Files
Projects-Portfolio/specs/2026-08-11-release-workflow-design.md
T
Keith Solomon fb6c9d4d61 Add design spec: GitHub Actions release-zip workflow
Single workflow file at .github/workflows/release.yml that triggers
on v* tag pushes, assembles projects-portfolio-v<version>.zip from
an allowlist of runtime files, uploads as a workflow artifact, and
attaches to a matching GitHub release.
2026-08-11 06:43:47 -05:00

7.3 KiB

Release Workflow Design

  • Date: 2026-08-11
  • Plugin: Projects Portfolio (projects-wp)
  • Status: Approved design, awaiting implementation plan

1. Goals

  • A single tag push (v*) automatically produces a release-ready zip named projects-portfolio-v<version>.zip.
  • The zip contains exactly the files WordPress needs to install and use the plugin: projects-portfolio.php at the root of projects-portfolio/, with admin/, assets/, includes/, languages/, templates/ subdirectories plus README.md and LICENSE.
  • The same zip is uploaded as a workflow artifact AND attached to a GitHub release matching the tag.
  • README gets a short "Building a release" section explaining how the workflow runs and what it produces.

2. Non-goals

  • Publishing to WordPress.org — out of scope (different credentials, different approval workflow).
  • Cross-repo publishing or PAT-based auth.
  • Signing, cosign, or checksums for the zip.
  • Auto-bumping PROJECTS_PORTFOLIO_VERSION or the plugin header before a tag is cut. The workflow assumes the tag is created after the version has been bumped in-tree.
  • Backporting tags. The workflow only acts on the tag push, not on retags of older SHAs.

3. Architecture

A single GitHub Actions workflow file at .github/workflows/release.yml runs on every v* tag push. It performs four sequential steps inside a single job:

  1. Checkoutactions/checkout@v4 at the tag's commit.
  2. Build zip — bash script copies allowlisted runtime files into a staging directory dist/staging/projects-portfolio/, then runs zip -r from the staging root to produce dist/projects-portfolio-v<version>.zip.
  3. Upload artifactactions/upload-artifact@v4 with name projects-portfolio-v<tag> and path dist/projects-portfolio-*.zip. Fails if no file matches.
  4. Create releasesoftprops/action-gh-release@v2 with the tag, generated release notes, and the zip file. Replaces the asset if a release with the same tag already exists (default action-gh-release@v2 behavior).

The workflow uses the runner-provided GITHUB_TOKEN with contents: write permission. No additional secrets are needed.

3.1 Why a staging directory

WordPress's "Upload Plugin" zip-install expects <plugin-slug>/<plugin-file>.php at the zip root. A direct zip -r of the working tree would put everything at the zip root — no enclosing folder — which would fail to install. The staging directory ensures the zip's internal layout has projects-portfolio/ at the top with projects-portfolio.php inside it.

3.2 Files added

  • .github/workflows/release.yml — the workflow.
  • README.md — modified, with a new "Building a release" section after the existing "Connect Your Gitea Repo" section.

No PHP files are touched. The PHPUnit suite is unaffected (no test bootstrap or production code changes).

4. Trigger & permissions

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: write

workflow_dispatch is intentionally omitted. To re-run for an existing tag, use the GitHub UI's "Re-run all jobs" button.

5. Zip filename

projects-portfolio-v<version>.zip, where <version> is the tag name with the leading v stripped. Examples:

  • Tag v1.1.0projects-portfolio-v1.1.0.zip
  • Tag v2.0.0-rc1projects-portfolio-v2.0.0-rc1.zip

6. Allowlisted zip contents

The build step copies these paths from the working tree into dist/staging/projects-portfolio/:

projects-portfolio.php
README.md
LICENSE
admin/
assets/
includes/
languages/
templates/

The bundled includes/plugin-update-checker/ library IS included — it's a runtime dependency used by projects-portfolio.php.

7. Excluded paths

The build step does not copy any of these. Most are already not tracked in git; the explicit list prevents accidental inclusion if any are added later:

  • composer.json, composer.lock — dev-only.
  • vendor/ — Composer dev dependencies.
  • tests/, phpunit.xml, phpunit.xml.dist, .phpunit.result.cache — test infrastructure.
  • plans/, specs/ — design artifacts (not tracked, but listed for safety).
  • .github/ — the workflow file itself (would otherwise be included by the catch-all cp -r).
  • .gitignore, .vscode/, .claude/ — editor / project config (not tracked except .gitignore).

If the build script can't find a tracked source path it expects (e.g. projects-portfolio.php renamed), set -e fails the step.

8. Build step (verbatim)

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"

unzip -l prints the table of contents to the action log so the run is self-documenting.

9. README addition

Add a "Building a Release" section after the existing "Connect Your Gitea Repo" section:

## 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<version>.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.

10. Error handling

Failure Behavior
Tag doesn't match v* Workflow doesn't run.
cp -r finds a missing source path set -e fails the step; no zip, no release.
zip command fails set -e fails the step; no artifact upload, no release.
Artifact upload finds no file if-no-files-found: error fails the step.
Release already exists for tag softprops/action-gh-release@v2 replaces the asset and updates notes (default behavior).
Missing contents: write permission Job fails at checkout or release step with a clear permissions error.

11. Rollout

  1. Implement on a feature branch.
  2. Merge into main (the workflow file is tracked and applies to all future tags).
  3. To produce the first automated release: bump PROJECTS_PORTFOLIO_VERSION and the plugin header from 1.1.0 to 1.1.1, commit, tag v1.1.1, push the tag.
  4. Verify on the GitHub Actions run page that: artifact uploads, zip contents look right (via the unzip -l log line), GitHub release is created with the zip attached.

12. New translatable strings

None. The workflow file and README change introduce no user-facing strings.