Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:

- name: Verify committed dist/ + action.yml + docs are up-to-date
run: |
git diff --exit-code -- '**/dist/**' action.yml packages/build/action.yml docs/inputs.md || {
git diff --exit-code -- '**/dist/**' action.yml docs/inputs.md || {
echo "::error::Committed dist/ or generated action.yml/inputs.md is out of sync. Run 'yarn build && yarn gen' locally and commit."
exit 1
}
170 changes: 165 additions & 5 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
name: E2E

# Invokes the top-level composite against the test-fixtures/ projects on
# Invokes the top-level action against the test-fixtures/ projects on
# ubuntu/macos/windows. Asserts:
# - produced binary runs and prints the expected version
# - checksum sidecar matches the binary
# - archive round-trips (when compress != none)
# - step summary contains a row for the target
#
# Every job below uses `uses: ./`, which resolves against the checked-out
# workspace. That is NOT how a consumer reaches the action, and the difference
# hid a release-blocking bug once. `consumer-ref` is the job that closes the
# gap — do not let it rot.

on:
push:
branches: [main]
pull_request:
paths:
# `packages/**` covers packages/build/action.yml + every source, test, and
# dist/ under packages/. The action.yml entries below are the top-level
# composite and the two sub-actions whose directories are not under packages/.
# `packages/**` covers every source, test, and dist/ under packages/.
# The action.yml entries below are the top-level entrypoint and the two
# sub-actions whose directories are not under packages/.
- 'packages/**'
- 'scripts/**'
- 'action.yml'
Expand Down Expand Up @@ -108,7 +113,7 @@ jobs:
- run: yarn gen
- name: Verify committed action.yml matches source
run: |
git diff --exit-code -- action.yml packages/build/action.yml docs/inputs.md || {
git diff --exit-code -- action.yml docs/inputs.md || {
echo "::error::action.yml or docs/inputs.md is out of sync. Run 'yarn gen' locally and commit."
exit 1
}
Expand Down Expand Up @@ -537,3 +542,158 @@ jobs:
[ "$out" = 'stamp=rewritten-by-transform' ] || {
echo "::error::transform hook did not rewrite the packed source (got: $out)"; exit 1;
}

# ──────────────────────────────────────────────────────────────────────
# The job that exercises the action the way a consumer does: the action
# directory is NOT the workspace root.
#
# Every other job says `uses: ./`, which makes GITHUB_WORKSPACE and the
# action directory the same tree. That coincidence hid a release blocker:
# the top-level action was a composite delegating to `uses: ./packages/build`,
# and a composite resolves a local `uses:` against the *consumer's* workspace
# (actions/runner#1348). It passed here and would have failed for every real
# caller.
#
# `uses:` takes no expressions, so this cannot say `<owner>/<repo>@<sha>` for
# the commit under test. Checking the action out into a subdirectory
# reproduces the part that matters — the workspace root holds the fixture and
# nothing else, so an action reaching for `$GITHUB_WORKSPACE/packages/…`
# finds nothing. `consumer-download` below covers the real fetch path.
consumer-ref:
name: consumer (action in a subdirectory)
runs-on: ubuntu-latest
steps:
- name: Check out the fixture only — no packages/, no action.yml
uses: actions/checkout@v6
with:
sparse-checkout: test-fixtures/tiny-app-cjs
sparse-checkout-cone-mode: false

- name: Check out the action under test beside it
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
path: _action-under-test

- name: Assert the workspace root does not look like pkg-action
shell: bash
run: |
for path in action.yml packages dist; do
if [ -e "$path" ]; then
echo "::error::workspace root contains $path — this job no longer proves anything"
exit 1
fi
done
ls -la

- name: Run pkg-action from its subdirectory
id: build
uses: ./_action-under-test
with:
config: test-fixtures/tiny-app-cjs/package.json
targets: node22-linux-x64
compress: tar.gz
checksum: sha256,sha512

- name: Assert the binary runs and the digests are real
shell: bash
run: |
bin=$(echo '${{ steps.build.outputs.binaries }}' | jq -r '.[0]')
[ -x "$bin" ] || { echo "::error::not executable: $bin"; exit 1; }
echo "binary output: $("$bin")"

artifact=$(echo '${{ steps.build.outputs.artifacts }}' | jq -r '.[0]')
[ -f "$artifact" ] || { echo "::error::missing artifact $artifact"; exit 1; }

# The digests output must agree with a local recompute, not just exist.
name=$(basename "$artifact")
expected=$(echo '${{ steps.build.outputs.digests }}' | jq -r --arg n "$name" '.[$n].sha256')
actual=$(sha256sum "$artifact" | cut -d' ' -f1)
[ "$expected" = "$actual" ] || {
echo "::error::sha256 mismatch — output says $expected, file is $actual"; exit 1;
}
echo "sha256 verified: $actual"

- name: Assert PKG_CACHE_PATH was exported to the job
shell: bash
run: |
# Exported by the action's main step; a later step seeing it proves the
# cache directory is shared with anything else that shells out to pkg.
[ -n "${PKG_CACHE_PATH:-}" ] || { echo "::error::PKG_CACHE_PATH not exported"; exit 1; }
echo "PKG_CACHE_PATH=$PKG_CACHE_PATH"

# ──────────────────────────────────────────────────────────────────────
# The real download path: `uses: <owner>/<repo>@<ref>`, resolved by GitHub
# rather than read off the runner's disk. A literal ref is the only thing
# `uses:` accepts, so this can only test `@main` — which makes it a
# post-merge canary, not a PR gate. `consumer-ref` above is the PR gate.
consumer-download:
name: consumer (downloaded from @main)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: test-fixtures/tiny-app-cjs
sparse-checkout-cone-mode: false

- name: Run pkg-action as published
id: build
uses: yao-pkg/pkg-action@main
with:
config: test-fixtures/tiny-app-cjs/package.json
targets: node22-linux-x64
checksum: sha256

- name: Assert the binary runs
shell: bash
run: |
bin=$(echo '${{ steps.build.outputs.binaries }}' | jq -r '.[0]')
[ -x "$bin" ] || { echo "::error::not executable: $bin"; exit 1; }
echo "binary output: $("$bin")"

# ──────────────────────────────────────────────────────────────────────
# Carried over from the M-1 spikes: `runs.using: node24` has to resolve on a
# self-hosted runner too, where the runner's bundled node is whatever the
# admin installed rather than the hosted image's. Skipped unless the repo
# actually has a runner with these labels — a missing runner otherwise queues
# the job until it times out.
self-hosted-node24:
name: self-hosted node24 smoke
if: vars.HAS_SELF_HOSTED_LINUX == 'true'
runs-on: [self-hosted, linux, x64]
timeout-minutes: 20
steps:
- uses: actions/checkout@v6

- name: Report runner + node
shell: bash
run: |
echo "runner: ${RUNNER_NAME:-unknown} (${RUNNER_OS}/${RUNNER_ARCH})"
echo "node: $(node --version) at $(command -v node)"

- name: Run pkg-action
id: build
uses: ./
with:
config: test-fixtures/tiny-app-cjs/package.json
targets: node22-linux-x64
checksum: sha256

- name: Assert the binary runs
shell: bash
run: |
bin=$(echo '${{ steps.build.outputs.binaries }}' | jq -r '.[0]')
[ -x "$bin" ] || { echo "::error::not executable: $bin"; exit 1; }
echo "binary output: $("$bin")"

- name: Assert the post step cleaned up after the previous run
shell: bash
run: |
# Hosted runners get a fresh VM; self-hosted ones do not, so a leaked
# invocation temp dir accumulates here and nowhere else.
leaked=$(find "${RUNNER_TEMP}" -maxdepth 1 -name 'pkg-action-*' | wc -l)
echo "invocation temp dirs currently present: $leaked"
[ "$leaked" -le 1 ] || {
echo "::error::post cleanup is leaking temp dirs on self-hosted runners"; exit 1;
}
83 changes: 83 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Release

# Cuts a release with release-it: bump → sync workspace versions → codegen →
# bundle → CHANGELOG from Conventional Commits → commit → tag → GitHub release
# → move the floating major tag. See .release-it.json for the sequence.
#
# Runs here rather than on a maintainer's laptop for one reason: the release
# commit contains the `dist/` bundles that consumers actually execute, and this
# is the only place they are guaranteed to be built by the Node in
# .node-version — the same one CI tested. A laptop on a different minor emits a
# different bundle, and nothing downstream would notice.

on:
workflow_dispatch:
inputs:
increment:
description: 'Version increment (release-it). Leave as conventional to let the commits decide.'
type: choice
default: conventional
options:
- conventional
- patch
- minor
- major
dry-run:
description: 'Print what would happen and change nothing.'
type: boolean
default: true

permissions:
contents: write

concurrency:
group: release
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
# release-it reads the commit history to pick the bump and write the
# changelog, and move-major-tag.ts resolves the tag it just pushed.
fetch-depth: 0
fetch-tags: true

- uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: yarn

- run: yarn install --non-interactive

- name: Identify as the release bot
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'

# release-it's own before:init hook runs lint + typecheck + unit tests, so
# a release cannot be cut from a red tree.
- name: Run release-it
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
args=(--ci)
if [ "${{ inputs.increment }}" != 'conventional' ]; then
args+=("--increment=${{ inputs.increment }}")
fi
if [ "${{ inputs.dry-run }}" = 'true' ]; then
args+=(--dry-run)
fi
echo "release-it ${args[*]}"
yarn release-it "${args[@]}"

- name: Remind about the Marketplace
if: ${{ inputs.dry-run != true }}
run: |
echo "### Released" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Publishing to the GitHub Marketplace is a manual checkbox on the" >> "$GITHUB_STEP_SUMMARY"
echo "release page — the API cannot set it. Open the new release and tick" >> "$GITHUB_STEP_SUMMARY"
echo "\"Publish this Action to the GitHub Marketplace\"." >> "$GITHUB_STEP_SUMMARY"
58 changes: 58 additions & 0 deletions .release-it.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"$schema": "https://unpkg.com/release-it@21/schema/release-it.json",
"git": {
"commitMessage": "chore(release): v${version}",
"tagName": "v${version}",
"tagAnnotation": "pkg-action v${version}",
"requireBranch": "main",
"requireCleanWorkingDir": true,
"requireUpstream": true,
"push": true
},
"npm": {
"publish": false
},
"github": {
"release": true,
"releaseName": "v${version}",
"autoGenerate": false,
"comments": {
"submit": false
}
},
"hooks": {
"before:init": ["yarn lint", "yarn typecheck", "yarn test:unit"],
"after:bump": [
"node --experimental-strip-types scripts/sync-workspace-versions.ts",
"yarn gen",
"yarn build"
],
"after:release": "node --experimental-strip-types scripts/move-major-tag.ts ${version}"
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": {
"name": "conventionalcommits",
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance" },
{ "type": "refactor", "section": "Refactoring" },
{ "type": "docs", "section": "Documentation" },
{ "type": "build", "section": "Build & Dependencies" },
{ "type": "ci", "section": "CI" },
{ "type": "test", "hidden": true },
{ "type": "chore", "hidden": true },
{ "type": "style", "hidden": true }
]
},
"infile": "CHANGELOG.md",
"header": "# Changelog\n\nAll notable changes to `yao-pkg/pkg-action`. Generated from Conventional Commits — do not edit by hand.\n",
"ignoreRecommendedBump": false,
"strictSemVer": true,
"gitRawCommitsOpts": {
"from": "9781c17dbf8171995379f2ecf30c9d5a7ea1e592"
}
}
}
}
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Changelog

All notable changes to `yao-pkg/pkg-action`. Generated from Conventional Commits — do not edit by hand.
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ We run TypeScript directly via `node --experimental-strip-types`. **Do not** int

Hard caps: **6 runtime deps, 3 dev deps.** Every addition requires a justification comment next to its `package.json` entry. See the plan's §16 for the current allow-list and deny-list.

Current runtime deps (6/6 — the budget is full): `@actions/core`, `@actions/exec`, `@actions/cache`, `@actions/glob`, `resedit`, `yazl`. `@actions/cache` + `@actions/glob` arrived when the composite wrapper was removed and the `actions/cache` step had to move in-process; they cost ~1.5 MB of bundle between them. Anything further needs one of these to leave first.

## Commits + PRs

- Conventional Commits (`feat:`, `fix:`, `refactor:`, `test:`, `chore:`, `docs:`).
These are not decoration: `CHANGELOG.md` and the version bump are generated
from them by release-it. A `feat:` cuts a minor, a `fix:` cuts a patch, and a
`BREAKING CHANGE:` footer cuts a major. `test:`, `chore:` and `style:` are
hidden from the changelog.
- Rebase onto `main`, not merge.
- CI must be green before merge. `dist/` and generated `action.yml` files are checked via `git diff --exit-code` — run `yarn build` before committing.
- CI must be green before merge. `dist/` and the generated `action.yml` are checked via `git diff --exit-code` — run `yarn build && yarn gen` before committing.

## Milestones

Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ Homebrew tap, Scoop bucket, npm package — all live in the same
- `yarn build` — esbuild ESM bundle of each sub-action
- `yarn test` — `node --test` with `--experimental-strip-types`
- `yarn lint` — ESLint + Prettier
- `yarn release:dry` — release-it dry run (see [`docs/architecture.md`](./docs/architecture.md) §11)

Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) —
`CHANGELOG.md` and the version bump are generated from them.

## Security

Report vulnerabilities privately — see [`SECURITY.md`](./SECURITY.md), which
also documents the trust boundary around pkg build hooks (they are arbitrary
code execution sourced from the ref you build).

## License

Expand Down
Loading
Loading