Skip to content

feat(launcher): opt-in Rust engine behind the existing launcher (#262 Stage 1) #41

feat(launcher): opt-in Rust engine behind the existing launcher (#262 Stage 1)

feat(launcher): opt-in Rust engine behind the existing launcher (#262 Stage 1) #41

name: Action Marketplace readiness
# Marketplace-readiness pipeline for the composite action (action.yml,
# public display name "Owen lifetime/resource check" — public facade
# rebrand, PR #246) — separate from ci.yml's own dog-fooding
# (`own-check-codescan`, which uses `uses: ./` against Own.NET's own sample
# tree on every push/PR). This workflow proves the CONSUMER-facing surface
# against a small dedicated fixture that looks like an ordinary user's repo
# (not the precision-test corpus), plus the immutable-tag / moving-major-tag
# release handling. Also uses `uses: ./` — GitHub Actions does not evaluate
# expressions in `steps.uses`, so a genuinely dynamic remote
# `owner/repo@<this commit>` reference isn't achievable pre-tag; see
# docs/notes/action-marketplace-readiness.md for the full account of what
# that leaves unproven and why.
permissions:
contents: read
on:
push:
branches: ["**"]
paths:
- "action.yml"
- "scripts/own-check.sh"
- "scripts/own-check.ps1"
- "fixtures/marketplace-consumer-demo/**"
- ".github/workflows/action-marketplace-readiness.yml"
tags:
- "v*.*.*"
pull_request:
paths:
- "action.yml"
- "scripts/own-check.sh"
- "scripts/own-check.ps1"
- "fixtures/marketplace-consumer-demo/**"
- ".github/workflows/action-marketplace-readiness.yml"
workflow_dispatch:
inputs:
move_major_tag_to:
description: >-
Release tag (e.g. v0.1.0) to point the moving major tag at. Leave
empty to just run the consumer-simulation checks.
required: false
default: ""
jobs:
# The consumer-facing proof, against a small dedicated fixture instead of
# Own.NET's own precision-test corpus (ci.yml's `own-check-codescan` dog-food
# job already covers that against frontend/roslyn/samples).
#
# Uses `uses: ./`, NOT a dynamic `uses: PhysShell/Own.NET@${{ github.sha }}`
# (Codex review, PR #245): `jobs.<job_id>.steps.uses` does not evaluate
# expressions — GitHub's own context-availability docs don't list it as a
# field expressions may appear in (unlike `with`/`env`/`if`/`run`), so that
# ref would have been treated as a literal (and-broken) string and the job
# would never have run at all. There is no way to parameterize `uses:` with
# "the commit currently under test" — a genuinely dynamic remote-ref proof
# isn't automatable pre-tag. `uses: ./` is the correct, honest mechanism
# here (same one ci.yml's dog-food job already relies on); the meaningful
# difference from that job is the fixture, not the resolution mechanism.
# See docs/notes/action-marketplace-readiness.md for the full account,
# including what a genuinely separate consumer repo would additionally
# prove and why one wasn't created here.
consumer-simulation:
name: consumer simulation — dedicated fixture, real findings
runs-on: ubuntu-latest
# security-events:write is required by upload-sarif (Codex review, PR
# #245: `security-events: write` is REQUIRED for every workflow that
# calls it, not just push events) — every other step only needs the
# default contents:read.
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Owen check on the leak sample (fails as configured)
id: leak
uses: ./
continue-on-error: true # expected to fail — asserted on explicitly below, not swallowed
with:
path: fixtures/marketplace-consumer-demo/Leaky.cs
format: github
fail-on-finding: "true"
- name: Assert the leak step actually failed (not silently green)
run: |
[ "${{ steps.leak.outcome }}" = "failure" ] \
|| { echo "FAIL: expected the leak-sample step to fail (fail-on-finding), got '${{ steps.leak.outcome }}'"; exit 1; }
echo "OK: consumer-style invocation found the leak and failed the step as configured"
- name: Owen check on the clean sample (must NOT fail)
uses: ./
with:
path: fixtures/marketplace-consumer-demo/Clean.cs
format: github
fail-on-finding: "true"
# P-035: the `config:` input must reach the extractor through the Action border.
# The weak-wrapper fixture is a real finding WITHOUT a declaration and silent WITH
# one, so the pair of runs proves the input plumbs (an Action-level test, not a
# direct scripts/own-check.sh call).
- name: Owen check — weak wrapper, NO config (finds the dropped token)
id: weaknocfg
uses: ./
continue-on-error: true # expected to fail — asserted below
with:
path: fixtures/marketplace-consumer-demo/WeakSubscribeConsumer.cs
format: github
fail-on-finding: "true"
- name: Assert the un-declared weak wrapper WAS flagged
run: |
[ "${{ steps.weaknocfg.outcome }}" = "failure" ] \
|| { echo "FAIL: weak wrapper without --config should be a finding, got '${{ steps.weaknocfg.outcome }}'"; exit 1; }
echo "OK: without a [weak-subscription] declaration the wrapper is a finding"
- name: Owen check — weak wrapper, WITH config (recognised, silent)
uses: ./
with:
path: fixtures/marketplace-consumer-demo/WeakSubscribeConsumer.cs
config: fixtures/marketplace-consumer-demo/own.toml
format: github
fail-on-finding: "true"
- name: Owen check — a malformed config is a hard error through the Action
id: badcfg
uses: ./
continue-on-error: true # expected to fail — asserted below
with:
path: fixtures/marketplace-consumer-demo/Clean.cs
config: fixtures/marketplace-consumer-demo/own-bad.toml
format: github
fail-on-finding: "false"
- name: Assert the malformed config failed the Action step
run: |
[ "${{ steps.badcfg.outcome }}" = "failure" ] \
|| { echo "FAIL: a malformed --config must hard-error through the Action, got '${{ steps.badcfg.outcome }}'"; exit 1; }
echo "OK: own.toml plumbs through the Action; a malformed config is a hard error"
# Fork PRs get a read-only GITHUB_TOKEN (GitHub's fork-PR token policy),
# so security-events:write is never actually granted no matter what
# this job requests — skip the SARIF/upload steps there instead of
# failing red for external contributors through no fault of their own
# (Codex review, PR #245; mirrors ci.yml's own-check-codescan guard).
# Same-repo pushes/PRs and tag pushes still run it.
- name: Owen check, SARIF surface + upload-sarif wiring
id: sarif
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: ./
with:
path: fixtures/marketplace-consumer-demo
format: sarif
severity: warning
fail-on-finding: "false"
- name: The action exposes a non-empty sarif-file output
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
run: |
f="${{ steps.sarif.outputs.sarif-file }}"
test -n "$f" || { echo "FAIL: sarif-file output not set"; exit 1; }
test -s "$f" || { echo "FAIL: sarif-file '$f' missing or empty"; exit 1; }
echo "OK: $(wc -c < "$f") bytes of SARIF"
- name: Upload to GitHub code scanning (the real consumer path)
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
with:
sarif_file: ${{ steps.sarif.outputs.sarif-file }}
category: action-marketplace-consumer-demo
# Immutable version tag: a pushed vX.Y.Z is never mutated once pushed —
# this job only VALIDATES it (re-runs the consumer-simulation checks
# implicitly via `needs`, plus a metadata sanity pass). It never moves any
# tag itself.
validate-release-tag:
name: validate the pushed release tag
# event_name == 'push' required alongside the ref check (same class of
# gap Codex flagged on the CLI's release workflow, PR #244): github.ref
# alone doesn't prove the run was actually caused by a tag push, since a
# workflow_dispatch run can be pointed `--ref` at an existing tag too.
# This job only validates (never mutates/publishes), so the stakes are
# lower than the CLI's publish gate, but the fix is the same and free.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: consumer-simulation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: action.yml has the fields Marketplace publishing requires
run: |
python3 - <<'PY'
import sys, re
text = open("action.yml", encoding="utf-8").read()
# Cheap structural checks (no YAML+schema dependency in this repo's
# zero-dependency Python core) - enough to catch an accidental
# metadata regression before it reaches a real tag/publish.
for field in ("name:", "description:", "author:", "branding:", "icon:", "color:"):
assert field in text, f"action.yml missing '{field}'"
m = re.search(r'icon:\s*"([^"]+)"', text)
c = re.search(r'color:\s*"([^"]+)"', text)
assert m, "branding.icon not found"
assert c, "branding.color not found"
allowed_colors = {"white", "yellow", "blue", "green", "orange", "red", "purple", "gray-dark"}
assert c.group(1) in allowed_colors, f"branding.color '{c.group(1)}' not in Marketplace's allowed set {allowed_colors}"
print(f"OK: action.yml metadata present; icon={m.group(1)!r} color={c.group(1)!r}")
PY
- name: Tag is immutable from here — this job only validates, never mutates
run: echo "Tag ${{ github.ref_name }} at ${{ github.sha }} validated. Moving the major tag is a SEPARATE, explicit workflow_dispatch step — see move-major-tag."
# The ONLY place a major tag (e.g. v0) is ever moved. Deliberately
# `workflow_dispatch`-only with an explicit target — never runs from a
# plain tag push, so pushing v0.1.1 can never silently repoint v0 as a
# side effect. Force-moving a tag is a hard-to-reverse, externally-visible
# action (equivalent to a force-push), so it is gated behind the
# `action-major-tag-move` GitHub Environment, which a repo admin must
# configure with required reviewers before this can run unattended.
move-major-tag:
name: move the major tag (protected, manual only)
if: github.event_name == 'workflow_dispatch' && inputs.move_major_tag_to != ''
runs-on: ubuntu-latest
environment: action-major-tag-move
permissions:
contents: write
# Required to call GET /repos/.../environments/{name} below (Codex
# review: "the environment-read API requires Actions read permission
# for fine-grained repository tokens") -- contents:write alone is not
# enough for that specific endpoint.
actions: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: true
fetch-depth: 0
# GitHub auto-creates a REFERENCED-BUT-NEVER-CONFIGURED environment on
# first use, with NO protection rules (Codex review: "referencing a
# missing environment creates it and the newly created environment has
# 'no protection rules or secrets configured'"). `environment:
# action-major-tag-move` above is therefore not itself proof a human
# ever approves this job — the job-dispatch gate GitHub evaluates
# BEFORE any step runs already let this run through if the
# environment was never actually configured with required reviewers.
# This step is the loud, fail-closed check for that: it runs right
# after checkout (before the force-push), and refuses to move the tag
# unless the environment has a REQUIRED_REVIEWERS rule with at least
# one reviewer (Codex review: a bare protection_rules count also
# accepts a wait_timer- or branch_policy-only environment, neither of
# which waits for a human) -- scripts/check_environment_protection.sh
# is the single source of truth for that predicate, shared with
# owen-cli-release.yml's analogous publish-job check and
# fixture-tested in ci.yml.
- name: Refuse to move the tag unless action-major-tag-move actually has protection rules
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api "repos/${{ github.repository }}/environments/action-major-tag-move" > "$RUNNER_TEMP/action-major-tag-move-env.json"
./scripts/check_environment_protection.sh "$RUNNER_TEMP/action-major-tag-move-env.json" \
|| { echo "::error::the 'action-major-tag-move' GitHub Environment does not have a required_reviewers rule with at least one reviewer -- a repo admin must set that up under Settings -> Environments before this workflow_dispatch can safely force-move a major tag. Refusing to proceed."; exit 1; }
# Inputs flow through the environment (data), not template-interpolated
# into the script body (code) — same pattern action.yml itself already
# follows (its own CodeRabbit #10 fix) and CodeRabbit flagged here too
# (PR #245): a `move_major_tag_to` containing shell metacharacters would
# otherwise expand before bash parses the script. The SemVer format
# check below is a second, independent guard, not a substitute for it.
- name: Resolve and validate the target release tag
id: target
env:
MOVE_MAJOR_TAG_TO: ${{ inputs.move_major_tag_to }}
run: |
target="$MOVE_MAJOR_TAG_TO"
echo "$target" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' \
|| { echo "FAIL: '$target' is not a valid vX.Y.Z tag"; exit 1; }
git rev-parse "refs/tags/$target" >/dev/null 2>&1 \
|| { echo "FAIL: tag '$target' does not exist — push it first, this job never creates release tags"; exit 1; }
major="${target%%.*}" # "v0.1.0" -> "v0"
echo "target=$target" >> "$GITHUB_OUTPUT"
echo "major=$major" >> "$GITHUB_OUTPUT"
- name: Force-move the major tag to the target release commit
env:
TARGET: ${{ steps.target.outputs.target }}
MAJOR: ${{ steps.target.outputs.major }}
run: |
sha=$(git rev-parse "refs/tags/$TARGET^{commit}")
echo "Moving $MAJOR -> $TARGET ($sha)"
git tag -f "$MAJOR" "$sha"
git push origin "$MAJOR" --force