From 831271fcdf1a689330c9bc583c1bbd676df87e05 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 19:07:07 +0000 Subject: [PATCH] feat(action): Marketplace-readiness pipeline, rebuilt against the Owen facade (P-013 gate B) Rebase of PR #248 onto post-#247 main. New workflow (.github/workflows/action-marketplace-readiness.yml): consumer-simulation (uses: ./ against fixtures/marketplace-consumer-demo, not the precision-test corpus) -> validate-release-tag (immutable vX.Y.Z, gated on both event_name == 'push' and the ref prefix) -> move-major-tag (workflow_dispatch-only, environment-gated via the shared scripts/check_environment_protection.sh predicate #247 introduced, template-injection-safe input handling). docs/notes/action-marketplace-readiness.md documents the versioning policy, the consumer-simulation fixture rationale, and two prior review corrections (environment auto-creation with zero rules; predicate tightened to required_reviewers-only). README.md/README.ru.md get a casing fix (PhysShell/own.net -> PhysShell/Own.NET) and a versioning-policy pointer. Final review hardening: git rev-parse "refs/tags/$TARGET" returns an annotated tag's own SHA, not the commit it points at. Peeled to ^{commit} so the major tag always ends up on the release commit rather than on the tag object itself. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SSXTDuh1ZHdQc4QqmYwshw --- .../action-marketplace-readiness.yml | 244 ++++++++++++++++++ README.md | 7 +- README.ru.md | 6 +- docs/notes/action-marketplace-readiness.md | 187 ++++++++++++++ fixtures/marketplace-consumer-demo/Clean.cs | 12 + fixtures/marketplace-consumer-demo/Leaky.cs | 13 + fixtures/marketplace-consumer-demo/README.md | 33 +++ 7 files changed, 500 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/action-marketplace-readiness.yml create mode 100644 docs/notes/action-marketplace-readiness.md create mode 100644 fixtures/marketplace-consumer-demo/Clean.cs create mode 100644 fixtures/marketplace-consumer-demo/Leaky.cs create mode 100644 fixtures/marketplace-consumer-demo/README.md diff --git a/.github/workflows/action-marketplace-readiness.yml b/.github/workflows/action-marketplace-readiness.yml new file mode 100644 index 0000000..d756c81 --- /dev/null +++ b/.github/workflows/action-marketplace-readiness.yml @@ -0,0 +1,244 @@ +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@` 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..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" + + # 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 diff --git a/README.md b/README.md index 2ad5b3f..c8f1397 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,17 @@ release. ```yaml - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -- uses: PhysShell/own.net@main # pre-release: no tagged release yet — pin a commit SHA for reproducibility +- uses: PhysShell/Own.NET@main # pre-release: no tagged release yet — pin a commit SHA for reproducibility with: format: github # inline PR annotations; use "sarif" for the Security tab fail-on-finding: "true" ``` +Once a release ships, prefer a pinned tag (`@v0.1.0`) or the moving major tag +(`@v0`) over `@main` — see +[`docs/notes/action-marketplace-readiness.md`](docs/notes/action-marketplace-readiness.md) +for the versioning policy. + ## Or point it at a repo you already have ```bash diff --git a/README.ru.md b/README.ru.md index 5160ed9..3efff57 100644 --- a/README.ru.md +++ b/README.ru.md @@ -14,12 +14,16 @@ ```yaml - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -- uses: PhysShell/own.net@main # пре-релиз: тегов ещё нет — для воспроизводимости пиньте commit SHA +- uses: PhysShell/Own.NET@main # пре-релиз: тегов ещё нет — для воспроизводимости пиньте commit SHA with: format: github # инлайн-аннотации в PR; "sarif" — для вкладки Security fail-on-finding: "true" ``` +После первого релиза предпочитайте закреплённый тег (`@v0.1.0`) или +подвижный major-тег (`@v0`) вместо `@main` — политика версионирования в +[`docs/notes/action-marketplace-readiness.md`](docs/notes/action-marketplace-readiness.md). + ## Или локально, на репозитории, который уже есть ```bash diff --git a/docs/notes/action-marketplace-readiness.md b/docs/notes/action-marketplace-readiness.md new file mode 100644 index 0000000..2757e9a --- /dev/null +++ b/docs/notes/action-marketplace-readiness.md @@ -0,0 +1,187 @@ +# GitHub Action Marketplace readiness (P-013 gate B) + +Companion to [`docs/notes/owen-cli-release.md`](owen-cli-release.md) +(the NuGet-package side of release readiness) — this note covers `action.yml`, +the composite action at the repo root, publicly displayed as **"Owen +lifetime/resource check"** (public facade rebrand, +[`docs/notes/owen-public-facade.md`](owen-public-facade.md) / PR #246). As +with the CLI note, **no Marketplace listing was published**; this documents +what was validated and what a maintainer still has to do to actually list it. + +## Metadata / branding audit + +`action.yml`'s Marketplace-required fields were checked against GitHub's +actual constraints, not just "does it have the keys": + +- `name: "Owen lifetime/resource check"`, `description`, `author` — present, + accurate to what the action does. `author: "Own.NET"` is deliberately kept + as the repository/project identity (PR #246's own documented decision, not + reopened here) while `name` carries the public Owen branding. +- `branding.icon: "shield"` — a real [Feather icon](https://feathericons.com/), + in Marketplace's allowed icon set. +- `branding.color: "purple"` — in Marketplace's fixed 8-color list (`white`, + `yellow`, `blue`, `green`, `orange`, `red`, `purple`, `gray-dark`). +- `description` is 230 characters flattened — Marketplace's card view + truncates around ~125; left as-is since shortening it would cost real + information and this is a cosmetic nicety, not a publish blocker. + +Both nested `uses:` steps inside the composite action +(`actions/setup-python`, `actions/setup-dotnet`) are pinned to exact commit +SHAs — done by PR #246 alongside the rest of the public facade rebrand, not +redone here; re-verified as still correct (matching `ci.yml`'s own pins). + +## Versioning policy — immutable release tag + explicitly-gated moving major tag + +Distinct tag namespace from the CLI's **`owen-cli-v*`** +([`docs/notes/owen-cli-release.md`](owen-cli-release.md)) — same repo, two +release surfaces, a shared bare `v*` would be ambiguous about which artifact +a tag names: + +- **Action release tags are bare SemVer: `vMAJOR.MINOR.PATCH`** (e.g. + `v0.1.0`) — the form GitHub's own "Releasing and maintaining actions" guide + documents, and what a consumer's `uses: owner/repo@v1` expects to resolve. +- **Immutable once pushed.** `.github/workflows/action-marketplace-readiness.yml`'s + `validate-release-tag` job runs on a `v*.*.*` tag push and only *validates* + (metadata sanity + the consumer-simulation checks via `needs:`) — it never + writes to the repository. +- **The moving major tag (`v0` while pre-1.0, `v1` after) only ever moves + through a separate, explicit `workflow_dispatch`** (`move-major-tag` job, + input `move_major_tag_to: vX.Y.Z`) — never as a side effect of pushing a + patch release. Force-moving a tag is equivalent to a force-push (rewrites + what a consumer pinned to the major tag gets next), so the job additionally + requires the `action-major-tag-move` GitHub Environment — **a repo admin + must configure that with required reviewers before this can run + unattended; it does not exist yet.** **Correction (Codex review):** + referencing a never-configured environment auto-creates it with zero + protection rules, so `environment: action-major-tag-move` alone is not + proof a human ever approves this job. The `move-major-tag` job now checks + out the repo, fetches the environment via `gh api`, and refuses to + force-move the tag — right after checkout, before the force-push — unless + it has a `required_reviewers` rule with at least one reviewer. + **Second correction (Codex review):** a bare `protection_rules` count + accepted a `wait_timer`- or `branch_policy`-only environment too, neither + of which waits for a human, and a `required_reviewers` rule can itself be + saved with zero reviewers. The check now calls + `scripts/check_environment_protection.sh` — the same small, fixture-tested + predicate `owen-cli-release.yml`'s `publish` job uses for `nuget-release` + — which only accepts a `required_reviewers` rule with >=1 reviewer. The + job's `permissions:` also gained `actions: read`, which the + environment-read endpoint requires alongside `contents: write`. See + `docs/notes/owen-cli-release.md`'s "Testing the environment-protection + predicate" for the fixture-driven `ci.yml` job that exercises this on + every ordinary push/PR, offline. + **Third correction (final review):** the SHA resolution + `git rev-parse "refs/tags/$TARGET"` returns the annotated tag object's own + SHA when `$TARGET` is an annotated tag, not the commit it points at — + moving the major ref onto that SHA would make it a tag-of-a-tag rather + than a tag pointing at the release commit. `^{commit}` peels an annotated + tag to the commit it references (a no-op on a lightweight tag, which + already points at a commit), so the major tag always ends up pointing at + a commit either way. +- Both `github.event_name == 'push'` *and* the ref-prefix check gate the + tag-triggered jobs — `github.ref` alone doesn't prove a tag was actually + pushed, since a `workflow_dispatch` run can be pointed `--ref` at an + existing tag too (the same class of gap independent review caught on the + CLI's publish gate, PR #244 — fixed here from the start). + +## Consumer-simulation fixture + +`fixtures/marketplace-consumer-demo/` — deliberately separate from +`frontend/roslyn/samples/` (Own.NET's own extractor precision-test corpus, +not representative of an ordinary consumer's code): one file with an +unambiguous leak (`Leaky.cs`, `OWN001`), one clean negative control +(`Clean.cs`) — diagnostic codes untouched by the public facade rebrand, per +its own explicit scope boundary. `action-marketplace-readiness.yml`'s +`consumer-simulation` job runs the action against it via `uses: ./`. + +**On the resolution mechanism:** an earlier version of this job tried +`uses: PhysShell/Own.NET@${{ github.sha }}`, intending a genuinely remote, +resolved-by-the-runner reference instead of a local path. That does not +work: GitHub Actions does not evaluate expressions in +`jobs..steps.uses` at all (it is not among the fields the +context-availability docs list as expression-capable, unlike `with`/`env`/ +`if`/`run`) — the string would have been passed through literally and the +job would never have resolved an action, let alone run one (Codex review, +PR #245, caught this before it ever merged). There is no mechanism to +parameterize `uses:` with "the commit currently under test"; a genuinely +dynamic remote-ref proof is not automatable in a pre-tag workflow. `uses: +./` is the correct, honest mechanism this workflow has used from the +start of what actually shipped — the same one `ci.yml`'s +`own-check-codescan` job already relies on. The meaningful difference from +that job is the fixture (a small consumer-style pair, not the +precision-test corpus) and the release/tag-validation wiring around it, +not the resolution mechanism, which was never a real option here. + +Verifies: the leak sample fails the step (`fail-on-finding: true`, asserted +via `steps.leak.outcome`), the clean sample does not, and the SARIF surface +produces a non-empty `sarif-file` output that `github/codeql-action/upload-sarif` +accepts (skipped on fork PRs, which get a read-only `GITHUB_TOKEN` that can +never satisfy `security-events: write` — same guard `own-check-codescan` +already uses, applied here after Codex flagged the same gap on this job). +The SARIF file itself carries the `Owen` driver name and the default +`owen.sarif` filename (`action.yml`'s own default, PR #246) — this workflow +does not override either. + +**Honest limitation:** even `uses: ./` is not a full substitute for a +genuinely separate consumer repository (which would additionally prove +resolution against an entirely different git remote/identity, and would be +the only way to actually exercise a real `owner/repo@vX.Y.Z` reference). +A second public repository was not created for this — creating a new public +repo is a visible, user-facing action, not this session's call to make +without being asked. If a maintainer wants that stronger proof, cloning +`fixtures/marketplace-consumer-demo/` into a throwaway public repo and +pointing the 6-line README snippet at a real pushed tag is a five-minute +follow-up, and remains the one genuine way to exercise dynamic remote +`uses:` resolution — a real post-publication gate for whoever runs the +first actual release, not something to fake here in its place. + +`ci.yml`'s pre-existing `own-check-codescan` job (dog-fooding via `uses: ./` +against `frontend/roslyn/samples` on every push/PR) is untouched — this +workflow is the release-readiness path, not a replacement for that fast +per-push proof. + +## README accuracy + +Fixed a repo-name casing drift in both `README.md` and `README.ru.md`: +`PhysShell/own.net@main` → `PhysShell/Own.NET@main` (GitHub resolves both +case-insensitively, but a Marketplace listing and consumer-facing docs +should use the actual casing). Added a pointer to this note's versioning +policy so the "6-line" snippet tells a reader what to switch to once a +release exists, instead of only describing the pre-release state. + +## Marketplace publish checklist (not run — for whoever does) + +1. Confirm a `LICENSE` exists (same blocker `owen-cli-release.md` + tracks for the NuGet package — Marketplace listing requires a license on + the repo too). **This is the repository owner's decision, not this + session's.** +2. Push a `vX.Y.Z` tag; confirm `validate-release-tag` and + `consumer-simulation` both pass on it. +3. `workflow_dispatch` → `move-major-tag` with that tag, approve the + `action-major-tag-move` environment gate (once configured). +4. Update the README's "Run it in CI" snippet to the real tag (`@v0` or + `@v0.1.0`), replacing the `@main` pre-release note. +5. List on Marketplace via the repository's own Release UI ("Publish this + Action to the GitHub Marketplace" checkbox on a Release) — a manual, + human step; nothing in this repo's workflows does this automatically, by + design (Marketplace listing is a one-way, account-linked action). +6. For the strongest possible consumer proof, clone the fixture into a + throwaway public repo pointed at the real tag (see "Honest limitation" + above) — genuinely optional, but the only way to exercise a real dynamic + `owner/repo@vX.Y.Z` reference at all. + +## Boundaries honored + +- Not published to Marketplace. +- No license chosen on the repository owner's behalf. +- No analyzer logic touched — `scripts/own-check.sh`/`.ps1` and the core + detectors are unchanged; only `action.yml`'s nested step pins (PR #246), + the release/consumer-simulation workflow, the fixture, and doc/README + accuracy changed. +- No failure hidden behind `continue-on-error`: the one place it's used + (`consumer-simulation`'s leak-sample step) is immediately followed by an + explicit assertion on `steps.leak.outcome` — the point is to observe and + check a *specific expected* failure, not to swallow an unexpected one. +- No second public repository was created for the stronger consumer proof + described above — that is a visible, user-facing action left for the + repository owner to take if/when they want it, not simulated here. diff --git a/fixtures/marketplace-consumer-demo/Clean.cs b/fixtures/marketplace-consumer-demo/Clean.cs new file mode 100644 index 0000000..62f4daf --- /dev/null +++ b/fixtures/marketplace-consumer-demo/Clean.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace ConsumerDemo; + +public class Tidy +{ + public void Run() + { + using var buffer = new MemoryStream(); + buffer.WriteByte(1); + } +} diff --git a/fixtures/marketplace-consumer-demo/Leaky.cs b/fixtures/marketplace-consumer-demo/Leaky.cs new file mode 100644 index 0000000..bbc0e69 --- /dev/null +++ b/fixtures/marketplace-consumer-demo/Leaky.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace ConsumerDemo; + +public class Leaky +{ + public void Run() + { + var buffer = new MemoryStream(); + buffer.WriteByte(1); + // no Dispose()/using -> OWN001 + } +} diff --git a/fixtures/marketplace-consumer-demo/README.md b/fixtures/marketplace-consumer-demo/README.md new file mode 100644 index 0000000..6c8f43d --- /dev/null +++ b/fixtures/marketplace-consumer-demo/README.md @@ -0,0 +1,33 @@ +# Marketplace consumer-simulation fixture + +This directory stands in for "a repo that installs the Owen GitHub +Action" (public display name "Owen lifetime/resource check" — public +facade rebrand, `docs/notes/owen-public-facade.md` / PR #246) — +deliberately small, deliberately *not* part of `frontend/roslyn/samples` +(Own.NET's own extractor precision-test corpus, which mixes many +deliberate positive/negative cases and is not representative of an +ordinary consumer's code). + +Used by `.github/workflows/action-marketplace-readiness.yml`'s +`consumer-simulation` job, via `uses: ./` (GitHub Actions does not evaluate +expressions in `steps.uses`, so a dynamic `uses: PhysShell/Own.NET@` isn't achievable — see `docs/notes/action-marketplace-readiness.md` +for the full account). The meaningful difference from `ci.yml`'s own +`uses: ./` dog-food job is this fixture: small and consumer-shaped, not +Own.NET's own precision-test corpus. + +- `Leaky.cs` — one intentional, unambiguous leak (`OWN001`): a + `MemoryStream` local that is never disposed. Exists to prove the action + actually finds something and annotates it, not just that it runs and + exits cleanly. +- `Clean.cs` — the same shape, disposed correctly. Exists to prove the + action does *not* fail a normal, correct file (no false alarm on the + negative control). + +This is an honest approximation, not a full substitute for a genuinely +separate consumer repository — a real external repo would additionally +prove the action resolves for a checkout with an entirely different git +remote/identity. That was not created here without explicit authorization +(creating a second public repository is a visible, user-facing action); +see `docs/notes/action-marketplace-readiness.md` for the tradeoff this +records.