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.
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 namedprojects-portfolio-v<version>.zip. - The zip contains exactly the files WordPress needs to install and use the plugin:
projects-portfolio.phpat the root ofprojects-portfolio/, withadmin/,assets/,includes/,languages/,templates/subdirectories plusREADME.mdandLICENSE. - 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_VERSIONor 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:
- Checkout —
actions/checkout@v4at the tag's commit. - Build zip — bash script copies allowlisted runtime files into a staging directory
dist/staging/projects-portfolio/, then runszip -rfrom the staging root to producedist/projects-portfolio-v<version>.zip. - Upload artifact —
actions/upload-artifact@v4with nameprojects-portfolio-v<tag>and pathdist/projects-portfolio-*.zip. Fails if no file matches. - Create release —
softprops/action-gh-release@v2with the tag, generated release notes, and the zip file. Replaces the asset if a release with the same tag already exists (defaultaction-gh-release@v2behavior).
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.0→projects-portfolio-v1.1.0.zip - Tag
v2.0.0-rc1→projects-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-allcp -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
- Implement on a feature branch.
- Merge into
main(the workflow file is tracked and applies to all future tags). - To produce the first automated release: bump
PROJECTS_PORTFOLIO_VERSIONand the plugin header from1.1.0to1.1.1, commit, tagv1.1.1, push the tag. - Verify on the GitHub Actions run page that: artifact uploads, zip contents look right (via the
unzip -llog 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.