Product-neutral reference implementation for the VibeCode QA GitHub Action Package charter, composed with TypeScript v1, Testing v1, and Security v1.
The action itself is deliberately small: it validates a release tag against semantic
versioning and emits normalized version outputs. The point of the repository is not the
feature — it is the shape: a complete action.yml contract, validated inputs, a
reproducible committed bundle, stable exit behaviour, minimum-permission examples, and CI
that runs the action the way a consumer runs it.
github-action-packageis a charter, not yet a versioned rubric. The page linked above carries candidate rules (R-GHA-1 … R-GHA-10) but nov1edition. This repo is the reference implementation those rules are being read against.
If you only need to build an action, start with GitHub's own path:
This repository is not a replacement for those. It is a VCQA fixture that shows how the charter is judged once metadata completeness, permissions, bundle freshness, release tags, and consumer-shaped smoke evidence are all required at once.
Minimum permissions: none. This action reads no repository data, calls no API, and needs
no GITHUB_TOKEN, so an empty permissions: block is enough. Every example below states its
permissions explicitly rather than inheriting the workflow default.
name: Release gate
on:
push:
tags: ['v*']
permissions: {}
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: vibecodeqa/ref-github-action-package@v1
id: tag
with:
tag: ${{ github.ref }}
allow-prerelease: 'false'
# Hyphenated output names need index syntax; `outputs.major-tag` would parse as subtraction.
- env:
VERSION: ${{ steps.tag.outputs.version }}
MAJOR_TAG: ${{ steps.tag.outputs['major-tag'] }}
run: echo "Releasing $VERSION (major tag $MAJOR_TAG)"name: Tag report
on:
workflow_dispatch:
inputs:
candidate:
description: Tag to inspect
required: true
permissions: {}
jobs:
inspect:
runs-on: ubuntu-latest
steps:
- uses: vibecodeqa/ref-github-action-package@v1
id: tag
with:
tag: ${{ inputs.candidate }}
fail-on-invalid: 'false'
- if: steps.tag.outputs.valid != 'true'
env:
REASON: ${{ steps.tag.outputs.reason }}
run: echo "Rejected because $REASON"Privileged consumers — anything running with contents: write, id-token: write, or a
secret in scope — should pin the action by commit SHA rather than by tag:
name: Privileged release
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
# vibecodeqa/[email protected]
- uses: vibecodeqa/ref-github-action-package@0000000000000000000000000000000000000000
with:
tag: ${{ github.ref }}Replace the zero SHA with the commit the release tag points at. contents: write above is
required by the surrounding release job, not by this action.
| Input | Required | Default | Description |
|---|---|---|---|
tag |
yes | — | Tag to validate. Accepts v1.2.3 or refs/tags/v1.2.3, so github.ref can be passed straight through. |
prefix |
no | v |
Required leading prefix before the version core. Set to '' to accept bare versions. |
allow-prerelease |
no | true |
Whether pre-release tags such as v1.2.3-rc.1 are accepted. |
fail-on-invalid |
no | true |
Whether an invalid tag fails the step, or is reported through outputs instead. |
| Output | Description |
|---|---|
valid |
"true" when the tag passed validation and policy. |
reason |
ok, invalid-input, missing-prefix, invalid-semver, or prerelease-not-allowed. |
version |
Canonical version without the prefix, e.g. 1.2.3-rc.1. |
major / minor / patch |
Version numbers as strings, or "" when invalid. |
prerelease |
Pre-release identifiers such as rc.1, or "". |
build |
Build metadata such as 20260809, or "". |
major-tag |
Moving major tag a release workflow should update, e.g. v1. |
is-prerelease |
"true" when the version carries pre-release identifiers. |
Every output is written on every path, success or failure, so consumers never have to guess whether a key exists.
Failure signalling is part of the contract, not an implementation detail:
| Situation | Annotation | Step result |
|---|---|---|
| Valid tag | ::notice |
success |
Invalid tag, fail-on-invalid: true (default) |
::error — Invalid tag [reason] |
failure |
| Pre-release rejected by policy | ::error — Policy rejection [prerelease-not-allowed] |
failure |
Invalid tag, fail-on-invalid: false |
::warning |
success, valid=false |
| Malformed action inputs | one ::error per problem |
failure, always |
An expected policy rejection and an unexpected runtime failure are distinguishable from the log line alone. Malformed inputs always fail: a misconfigured gate is not a gate.
- The action declares no secret inputs and reads no environment variable other than
the
INPUT_*values the runtime sets for its declared inputs.src/main.tscontains noprocess.envaccess at all, and a unit test asserts that. - It never reads
GITHUB_TOKENand makes no network calls, sopermissions: {}is sufficient. - Input values are never interpolated into a shell string. The action is a Node process; the
tag is parsed by an anchored, non-backtracking grammar in
src/semver.tsbefore anything else looks at it. - If you fork this and add a token-shaped input, register it with
core.setSecret()at first use and keep it out of outputs, job summaries, and uploaded artifacts.
- Runtime is pinned in
action.yml:runs.using: node24,runs.main: dist/index.js. - The package manager is pinned by
packageManagerinpackage.json([email protected]), and CI installs with--frozen-lockfileagainst the committedpnpm-lock.yaml. - One production dependency:
@actions/core, pinned to an exact version. Dev dependencies are pinned to exact versions too — no^ranges anywhere. dist/index.jsis committed because a consumer never builds the action. It is produced only bypnpm build(esbuild, pinned) and CI fails if a clean rebuild differs by one byte.- Third-party actions used inside this repo's own workflows are pinned by commit SHA with the human-readable version in a trailing comment.
CI runs two blocking gates on every push and pull request:
pnpm audit --audit-level=moderate— fails on any moderate, high, or critical advisory.node scripts/check-licenses.mjs— fails on any resolved package whose licence is outside the allowlist (MIT,ISC,Apache-2.0,BSD-2-Clause,BSD-3-Clause,0BSD,CC0-1.0,Unlicense,BlueOak-1.0.0,Python-2.0).
Exception policy. Low-severity advisories are reported by pnpm audit but do not block;
they are triaged at the next dependency review. Anything moderate or above blocks the build.
If an advisory has no fix available, the only accepted route is an acceptedException record
in this README naming the advisory, the owner, the compensating control, and an expiry date —
never a silenced or continue-on-error job. There are no active exceptions today.
Releases are cut by pushing tags; nothing is published from a developer's machine.
- Update
CHANGELOG.mdand merge tomain. - Verify locally:
pnpm verify(lint, typecheck, tests, metadata validation, bundle build). - Confirm
git statusis clean — a dirtydist/means the bundle was never rebuilt. - Tag the exact commit with an immutable version tag:
git tag v1.2.3 && git push origin v1.2.3. AvX.Y.Ztag is never moved or deleted once pushed. - Move the major tag to the same commit:
git tag -f v1 <sha> && git push --force origin v1.v1is a moving pointer by design; it is documented as moving, and it only ever moves forward within the same major version. - Publish a GitHub release against the
vX.Y.Ztag.
Consumer guidance: @v1 for convenience, @v1.2.3 for reproducibility, and a full commit
SHA for anything privileged. That ordering is stated in the usage examples above.
corepack enable
pnpm install --frozen-lockfile
pnpm verify # lint + typecheck + test + action metadata + build
pnpm audit --audit-level=moderate
pnpm check:licensespnpm build regenerates dist/index.js. Commit the result: CI rebuilds and runs
git diff --exit-code -- dist so a stale bundle cannot ship.
| Path | Role |
|---|---|
action.yml |
The public contract: inputs, outputs, branding, runtime. |
src/semver.ts |
Anchored, dependency-free semantic-version grammar. |
src/inputs.ts |
Input parsing and validation — runs before any behaviour. |
src/evaluate.ts |
Pure decision core, no Actions runtime import. |
src/main.ts |
The only module that touches @actions/core. |
dist/index.js |
Generated bundle. Never edited by hand. |
scripts/validate-action-metadata.mjs |
Metadata completeness and declared-versus-read drift. |
scripts/check-licenses.mjs |
Dependency licence allowlist gate. |
tests/fixtures/ |
Behaviour and input-validation fixture tables. |
Tests need no repository, no runner, and no token: src/main.test.ts drives the real
@actions/core protocol through INPUT_* environment variables and a temporary
GITHUB_OUTPUT file.
.github/workflows/ci.yml runs, on every push and pull request:
| Job / step | What it proves |
|---|---|
pnpm install --frozen-lockfile |
The lockfile is authoritative. |
pnpm lint, pnpm typecheck |
Biome and TypeScript strict mode are clean. |
pnpm test |
Unit and protocol tests pass. |
pnpm validate:action |
action.yml is complete, its runtime is supported, it has not drifted from the source, and every README workflow example declares permissions:. |
pnpm build + git diff --exit-code -- dist |
The committed bundle matches a clean rebuild. |
pnpm audit, pnpm check:licenses |
Dependency and licence gates. |
smoke job |
Runs uses: ./ for a success case, a policy-rejection case, a malformed-tag case, and a non-fatal case, asserting outputs and step conclusions. |
The smoke job is a required check, not a reporting job: if the action stops behaving the way a consumer sees it, CI goes red.
The tracked report lives at docs/vcqa-report.md.
MIT — see LICENSE.