Skip to content

feat: complete the template - self-validating CI, security defaults, rehearsed adoption - #6

Merged
Vad1mo merged 23 commits into
mainfrom
feat/template-hardening
Aug 21, 2026
Merged

feat: complete the template - self-validating CI, security defaults, rehearsed adoption#6
Vad1mo merged 23 commits into
mainfrom
feat/template-hardening

Conversation

@Vad1mo

@Vad1mo Vad1mo commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked on #5. Merge that first; this branch already contains its commit plus the fixes for its open review threads.

What this closes

A gap analysis found the template was roughly 60% of a complete reference: it shipped files that were factually broken, documented files that did not exist, had zero CI validating itself, and was missing the entire security and supply-chain layer that harbor-scanner-trivy and dependencytrack-harbor-adapter already run in production.

An expert panel (six discipline reviews, adversarially judged for validity and for whether a consumer would keep or delete each item) then found things the gap analysis missed. Two were severe and both were verified against this repository rather than taken on trust.

The two severe ones

settings.yml published private repositories. It is PATCHed onto the repo by apply-settings on the first push to main, and it carried visibility: public and is_template: true. A repository created private from this template published itself, irreversibly. The fork guard that should have limited stray runs compared github.repository_owner to github.event.repository.owner.login — the same value on both sides, so always true.

The branch ruleset deadlocked every release. GitHub raises no workflow events for a ref pushed with GITHUB_TOKEN. Verified here: PR #4 was opened by release-please and the only run on that branch was PR Title, triggered later by an unrelated bot editing the PR. A required status check would never report on a release PR, and it could never be merged. The ruleset now requires a PR and no status checks, with the prerequisites documented.

What is new

The template validates itself. hygiene.yml (typos, actionlint, yamllint, zizmor, action-pin gate, dependency review) and ci.yml (build, race tests, lint, tidy, licence, govulncheck, container smoke test). .github/scripts/repo-lint.py adds 11 consistency checks — invalid YAML that tokenizes but does not construct, labels emitted but not declared, release-please packages that are not directories, docs linking to files that do not exist, pull_request_target workflows that check out code, committed binaries. Every check exists because that mistake actually shipped here; each was negative-tested.

Adoption is rehearsed, not hoped for. task bootstrap replaces the macOS-only sed one-liner. hygiene.yml bootstraps a scratch copy of HEAD down both adoption paths on every PR and asserts the result is clean. It found three real defects while being written.

Security on by default. SECURITY.md with a real CVD policy (no repo in the org had one), secret scanning with push protection, private vulnerability reporting, Dependabot, CodeQL, a branch ruleset, a DCO gate, every action SHA-pinned behind a gate.

Verifiable releases. cosign signatures, syft SPDX SBOMs for both binaries and images, build provenance, checksums, and copy-pasteable verify commands appended to the release body. This is the CRA Art. 24 evidence set.

Also fixed: sign-offs never reached main (squash_merge_commit_message: PR_BODY carries no trailer); release_created is path-scoped and silently skips publish jobs; --disallowed_types=...,permissive was dead config while reciprocal would fail on the first MPL-2.0 dependency; LICENSE shipped a hardcoded copyright line; the contributor-recognition stack was three broken mechanisms for one job, one of which pushed to main.

Verification

All gates pass on the template and on the bootstrapped output of both adoption paths:

repo-lint: 11 checks passed     typos ok     yamllint ok
actionlint ok                   pins ok      go build/vet ok
lang=go   -> repo-lint + yamllint + actionlint + typos
lang=none -> repo-lint + yamllint + actionlint + typos

Needs a human decision

  • .github/settings.yml is applied to the live repository. Review it before merge.
  • This repo's own release baseline: version.txt and the manifest are reset to 0.0.0, and the legacy tag is app-v1.1.0. See the version-baseline section in docs/RELEASES.md before the first release under the new scheme.
  • A SETTINGS_TOKEN secret is needed before apply-settings can do anything beyond labels.

Review in cubic

bupd and others added 10 commits August 4, 2026 13:06
Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
- release-please: grant issues: write. v5 labels its own release PR
  (autorelease: pending/tagged) through the Issues API, which is a
  separate scope from pull-requests.
- version baseline: ship 0.0.0 in version.txt and the manifest. A
  template must seed adopters at zero; 1.1.0 made every new repo start
  its first feat: release at 1.2.0.
- docs/RELEASES.md: add fix!: to the breaking-change row, stop claiming
  non-feat/fix types produce no release, and document that hidden: true
  is a changelog setting rather than a release gate. Add a dry-run
  command so the bump can be confirmed before merging.
- docs/RELEASES.md: document the version baseline rule. Switching from
  component tags (app-v1.1.0) to vX.Y.Z leaves no tag matching the
  manifest, so release-please walks the whole history and repeats old
  commits in the next changelog.
- apply-settings.js: collect section failures and fail the job instead
  of downgrading every one to a warning. A section that silently did
  not apply is drift nobody sees.
- apply-settings.js: distinguish 403/401 from not-configured when
  exporting. settings.yml always carries an actions block, so omitting
  it after an unreadable GET reported drift forever. Unreadable
  sections are now dropped from both sides of the comparison and
  warned about.
- CHECKLIST/README: include Taskfile.yml in the adoption copy lists -
  release-assets.yml runs task release-assets. Point the release-assets
  customization step at the Taskfile, note that new release jobs need
  their own permissions block under permissions: {}, and keep
  version.txt guidance out of the native release-type list.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
The template had no CI of its own: twelve workflows, a github-script
module and a Go program, none of it checked. That is how an invalid
issue-template config, a release workflow that could never fire, and
four dangling documentation references all shipped at once.

Gates:
- hygiene.yml runs typos, actionlint, yamllint, zizmor, a pin check and
  dependency-review on every push and pull request. Language-neutral,
  so it keeps working after the Go pack is deleted.
- ci.yml covers build, test, lint, licence check and govulncheck for
  the Go pack, and absorbs the old license-check workflow. govulncheck
  also runs weekly, since new advisories land against unchanged code.
- .github/scripts/repo-lint.py checks what a linter cannot: YAML that
  tokenizes but does not construct, JSON validity, labels the labeler
  emits that settings.yml does not declare, release-please packages
  that are not directories, version.txt against the manifest,
  documentation that links to files the repository does not ship, and
  placeholders left unreplaced after adoption. Each check exists
  because that mistake actually shipped here.
- versions.env pins every tool with a checksum where one is published,
  read by both the Taskfile and CI so local and CI runs agree.
- Taskfile grew build, test, lint, vuln-check, lint:yaml, lint:actions,
  lint:repo, lint:pins, license-report, clean and info. `task check`
  now runs the same gates CI does.
- lefthook runs the cheap gates pre-commit and `task check` pre-push.

Hardening, applied to every workflow:
- every action pinned to a full SHA with a version comment, enforced by
  the pin check
- permissions: {} at the top level with least-privilege job scopes
- persist-credentials: false on every checkout
- concurrency groups, cancel-in-progress false where a run must not be
  interrupted, and timeout-minutes everywhere
- fork guards on the jobs that write back or publish. The previous
  guard compared repository_owner to event.repository.owner.login,
  which is always true.

Defects fixed:
- ISSUE_TEMPLATE/config.yml: quote the placeholder URLs. Unquoted
  {{DOCS_URL}} is a flow mapping used as a mapping key, so GitHub
  rejected the whole template chooser.
- apply-settings.yml: drop the TODO forcing mode=apply. A scheduled run
  is now a drift check and everything else applies and verifies.
- settings.yml: enable Discussions, which SUPPORT.md, ROADMAP.md, the
  issue chooser and the welcome bot all link to.
- labeler.yml: component/config globbed *.yml at the root, so it landed
  on nearly every pull request. Narrowed to real config paths, and both
  labels it emits are now declared in settings.yml.
- delete current-settings.yml, a committed export of live state that
  guaranteed the drift check would fail.
- CHANGELOG.md: remove the placeholder tail referring to a workflow
  that does not exist.
- .gitignore: ignore only the local parts of .claude so shared agent
  configuration can ship.
- drop the dangling scripts/export-settings.sh references; drift
  detection is `gh workflow run apply-settings.yml -f mode=check`.

New files the gates demanded:
- SECURITY.md with a coordinated vulnerability disclosure policy,
  response targets, embargo terms, safe harbour and a supported-version
  table. No repository in the organisation had one.
- CONTRIBUTING.md, linked from SUPPORT.md and the welcome bot, which
  both 404'd.
- MAINTAINERS.md.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
No repository in the organisation had a SECURITY.md, secret scanning was
off, and no repository had branch protection. A template that leaves all
three to the adopter is where that came from.

Repository settings, applied by settings-as-code:
- secret scanning and push protection enabled. Both are free on public
  repositories and already the organisation default for new ones; push
  protection is the one that matters because it blocks the commit
  rather than reporting it afterwards.
- private vulnerability reporting enabled, which is what makes the
  advisory link in SECURITY.md and the issue chooser actually work.
  It is not part of security_and_analysis, so apply-settings.js now
  splits it out to its own endpoint and exports it for drift checks.
- a branch ruleset shipped active rather than commented out: pull
  request required, squash only, force push and deletion blocked,
  hygiene checks required, no bypass actors. Required approvals is 0
  deliberately - a rule a solo maintainer cannot satisfy gets deleted
  wholesale, and the protections that matter do not depend on it.

Supply chain:
- dependabot.yml for actions, Go modules and Docker. Pinning to SHAs
  without an updater just pins to something old; Dependabot rewrites
  the SHA and its version comment together.
- optional/renovate.json for projects that want grouped updates or need
  versions.env kept in sync, with a custom manager for the annotated
  pins. Documented as an either/or with Dependabot, not both.
- codeql.yml as advanced setup, matrix over actions and go. The
  `actions` language is worth keeping even for a non-Go project.
  settings.yml keeps code scanning default setup off so alerts are not
  duplicated.

Governance:
- CODE_OF_CONDUCT gets an enforcement address, a confidentiality
  commitment, and a recusal rule. An enforcement section with nobody to
  report to enforces nothing.
- CODEOWNERS with real rules, weighted towards .github and SECURITY.md
  since those change what runs with repository credentials.
- dco.yml enabling individual remediation commits, so a first-time
  contributor can add a missing sign-off without rewriting their branch.
- NOTICE, with a comment explaining what Apache-2.0 4(d) actually
  requires and what belongs in a licence report instead.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
The template published nothing verifiable. It also never built the
Dockerfile in CI, so the one container artifact it shipped was never
exercised. CRA Art. 24 steward duties from 11 Sep 2026 make release
SBOMs an obligation rather than a nice-to-have, and a bill of materials
that covers only the image does not describe the binaries most people
download.

Release artifacts (release-assets.yml):
- syft SPDX SBOM over the source tree, uploaded with the release
- actions/attest-build-provenance over the binaries
- checksums.txt, generated by the same task that builds them
- uses the local ./.github/actions/setup composite instead of a
  third-party setup-task action

Container image (publish-image.yml, new, workflow_call):
- multi-arch linux/amd64 and linux/arm64
- defaults to GHCR with GITHUB_TOKEN, so it works in a fresh repository
  with no secrets configured
- cosign keyless signature, syft SPDX attestation, and build provenance,
  all applied by digest rather than by tag, because a tag can be
  repointed after the signature is made
- buildx's own provenance and sbom are disabled to avoid two competing
  attestations for the same image

release-please.yml calls both on release_created, then a
document-artifacts job appends copy-pasteable cosign and gh attestation
verify commands to the release body. It replaces its marked section
rather than appending, so re-running does not duplicate it.

Dockerfile:
- base image pinned by digest, kept beside the tag in versions.env
- explicit non-root USER, OCI labels, -trimpath and a stamped version
- go.sum copied with a glob so the build works with or without
  dependencies

main.go now carries the version variable the build stamps and a
-version flag, with a test that guards the ldflags contract. The
pipeline had a version stamp pointing at a variable that did not exist.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Six discipline reviews (GitHub Actions supply chain, CISO/CRA
compliance, Go and containers, platform and release engineering, OSS
governance, adoption DX) produced 47 findings; a consumer advocate
judged each for validity, value, genericity and maintenance cost. This
lands the ones that survived. Two were severe and both were verified
against this repository rather than taken on trust.

Severe: settings.yml published private repositories
  settings.yml is PATCHed onto the repository by apply-settings on the
  first push to main, and it carried `visibility: public` and
  `is_template: true`. A repository created private from this template
  published itself, irreversibly, and became a template in the process.
  It also overwrote the adopter's description with the template's. All
  four keys are gone, with a comment saying why they must stay gone.
  The workflow's fork guard compared github.repository_owner to
  github.event.repository.owner.login, which is the same value on both
  sides and therefore always true; it now checks `fork`.

Severe: the shipped branch ruleset deadlocked every release
  GitHub raises no workflow events for a ref pushed with GITHUB_TOKEN.
  Verified here: PR #4 was opened by release-please and the only run on
  that branch was PR Title, triggered later by an unrelated bot editing
  the PR. Every other workflow produced zero runs. A required status
  check would therefore never report on a release pull request and it
  could never be merged, and the predictable response is to delete the
  ruleset rather than the release automation. The ruleset now requires
  a pull request and no status checks, with the prerequisites and the
  app-token fix documented in docs/RELEASES.md.

  hygiene.yml gains a `required-checks` aggregate job with no path
  filter, which is the only context safe to require: a path-filtered
  workflow does not report at all on a pull request that misses its
  filter, so the check waits forever.

Settings applier:
- vulnerability_alerts is now declarable. It is the precondition for
  dependabot_security_updates, which was declared without it.
- both endpoint-backed toggles are stripped from the PATCH body, which
  would otherwise 422 on unknown properties, and both are exported so
  the drift check compares symmetrically.
- labels are paginated. Past 100 the missing ones read as permanent
  drift.
- js-yaml is version-pinned and installed with --ignore-scripts, in the
  one job that can hold an administration token.
- without SETTINGS_TOKEN the job now reports what it skipped and
  succeeds. A new repository should not greet its owner with a red run
  it has no way to fix.

Sign-offs never reached main:
  squash_merge_commit_message was PR_BODY, and a pull request body
  carries no Signed-off-by trailer, so under squash-only merging every
  adopted repository had zero sign-offs in main while the README
  claimed otherwise. Now COMMIT_MESSAGES. Added dco.yml as an in-repo
  gate so the check exists without installing an app.

Licensing:
- LICENSE carried a hardcoded copyright line; now placeholders.
- `reciprocal` dropped from the disallowed set: it covers MPL-2.0, EPL
  and CDDL, and would fail on the first hashicorp dependency.
  `permissive` stays out - upstream defines it as an empty set, so
  listing it only looked like policy.

Release correctness:
- gate on `releases_created`, not `release_created`. The singular form
  is only emitted for the root package, so it disappears the moment a
  non-root package is added and every downstream job is skipped on a
  green run.
- exclude-paths: docs, and the inverse gotcha documented.
- removed the monorepo example from the README; the release workflows
  assume a single root package.

Workflow triggers:
- pr-title moved from pull_request_target to pull_request. It reads the
  title from the event payload and never needed a write token.
- the three workflows that genuinely need pull_request_target carry a
  loud do-not-check-out warning, and repo-lint parses the workflows and
  fails if any of them ever does. A comment is advice; this is a gate.
- zizmor pinned from versions.env; the action defaults to `latest`, so
  a new rule release could red main without a commit.
- zizmor SARIF upload and the whole Scorecard job now skip on private
  repositories instead of failing, and Scorecard no longer publishes a
  score before anyone has decided they want it cited.
- the pin gate also rejects `go install ...@latest` inside run blocks,
  which a perfect `uses:` check cannot see.

Deleted the contributor-recognition stack: CONTRIBUTORS.md carried one
named person's avatar, .all-contributorsrc configured a bot nobody
installed, and all-contributors.yml pushed commits to main for a
feature no adopter asked for. Three mechanisms for one job, none of
them working.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
…workflow self-explaining

Go pack, which until now was decoration: nothing compiled, vetted,
tested, linted or built anything the template shipped.
- .golangci.yaml, standard plus misspell and gofumpt. A starting point,
  not a policy an adopter turns off wholesale on day one.
- test runs with CGO_ENABLED=1. Some runner images export 0, and
  `go test -race` then quietly does not run the race detector.
- tidy-check fails when go.mod or go.sum is not tidy. It immediately
  found that go.mod shipped without a trailing newline.
- an image task and a CI image job that builds both architectures,
  loads the native one, runs it, and asserts the image is not root.
  The assertion deliberately does not depend on what the demo prints.
- the Dockerfile builder is --platform=$BUILDPLATFORM, so a linux/arm64
  build cross-compiles instead of running the whole Go toolchain under
  QEMU.

Optional packs are inert, not delete-by-hand. Every Go and Docker job
detects its pack inside the job and skips with a note in the summary.
Deleting release-assets.yml used to leave a dangling local
`uses: ./.github/workflows/release-assets.yml` and take release
automation down with it. Detection is in-job on purpose: a job-level
`if: hashFiles('go.mod') != ''` always fails open, because hashFiles
resolves against GITHUB_WORKSPACE and the workspace does not exist when
a job-level condition is evaluated.

Every workflow now opens with What / Fires on / Needs / Safe to delete
if. Six months from now the question asked of a red workflow is whether
it can just be deleted, and nothing in the repository answered it.

Code of Conduct is Contributor Covenant 2.1 in full. The previous file
was a 34-line abridgement that dropped Scope, Enforcement
Responsibilities and the four-tier Enforcement Guidelines, and closed
with a note addressed to whoever was editing the template rather than
to the community. The identical truncated file is already in
dependencytrack-harbor-adapter.

One support surface. Discussions is off and the links to it are gone;
questions go through a question.yml issue form. A second inbox that a
two-person team does not clear reads worse than not offering one. The
opt-in recipe is in the settings.yml comment.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
…output

coverage.out was committed by the previous commit because .gitignore
listed only bin, temp, .idea and .task. It now covers build output,
test and lint artifacts, and the files that must never be committed.

.gitattributes normalises line endings so a Windows checkout does not
produce a whole-file diff, and marks CHANGELOG.md generated so it stops
dominating diffs and language stats.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Adoption was a macOS-only `sed -i ''` one-liner that skipped .json,
.toml and Dockerfile, had no verification step, and left the reader to
work out which of twelve workflows applied to them. Nothing ever
exercised the template as a template, which is why a whole class of
defects only appeared in the adopter's repository.

task bootstrap (.github/scripts/bootstrap.py):
- substitutes every placeholder across all file types, prompting or
  taking flags
- removes whole marked blocks when an optional value is left blank.
  Dropping the line was not enough: it left the heading behind, and it
  left an issue-template contact link with no url, which GitHub rejects
  for the entire chooser
- strips template-only prose so the README stops linking to CHECKLIST.md
  after bootstrap deletes it
- --lang=none removes the Go pack and the release-please jobs that call
  the workflows it deleted
- writes .github/template.yml recording which template commit the
  repository came from, so it is possible to ask which repositories are
  behind and which still carry a defect fixed upstream
- deletes itself and CHECKLIST.md, then runs repo-lint, so a
  half-applied template fails loudly rather than quietly

The rehearsal, which is the point:
hygiene.yml now bootstraps a scratch copy of the current HEAD down both
adoption paths and asserts the result is a clean repository. Every other
gate reads files; this one walks the adopter's path, and it is the only
thing that can catch an interaction between two individually correct
pieces. It found three real defects while being written: the README
link to a file bootstrap deletes, the orphaned optional headings, and
the dangling workflow_call left by removing a pack.

repo-lint grew four checks, all of them things a grep in a workflow got
wrong by matching its own source: leftover bootstrap markers, local
workflow_call targets that do not resolve, contact links missing a
required field, and issue forms applying labels settings.yml does not
declare. Each was negative-tested by introducing the defect and
confirming the check fails.

Docs:
- README rewritten as a project README with the template instructions in
  a block bootstrap removes. It is no longer the only map of the
  automation, and no longer tells the reader to delete itself.
- docs/repo-automation.md is that map: every workflow, config file,
  script and secret, what it needs, when it is safe to delete, and the
  four things that will bite you.
- CHECKLIST.md is now only the decisions bootstrap cannot make, with the
  warning about required status checks that would otherwise deadlock
  every release.
- CLAUDE.md, with AGENTS.md as a symlink rather than a second copy that
  drifts, plus a shared .claude/settings.json. Enabled by narrowing
  .gitignore.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
An earlier commit in this branch shipped a 2.6 MB Mach-O binary. A bare
`go build ./...` writes one binary per main package into the working
directory, named after the package directory, and `git add -A` then
commits it. The template would have handed that to every adopter.

- build now writes to bin/, which is ignored
- .gitignore also ignores a root binary named after the repository,
  which is what go build produces by default
- repo-lint fails on any tracked file whose leading bytes are ELF,
  Mach-O or PE. Negative-tested by committing one and confirming the
  check catches it.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Vad1mo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f086cf0-e852-46e4-b7d0-40ccf70db521

📥 Commits

Reviewing files that changed from the base of the PR and between ee8bd78 and 984692d.

📒 Files selected for processing (61)
  • .all-contributorsrc
  • .claude/settings.json
  • .editorconfig
  • .gitattributes
  • .github/CODEOWNERS
  • .github/ISSUE_TEMPLATE/config.yml
  • .github/ISSUE_TEMPLATE/question.yml
  • .github/actions/setup/action.yml
  • .github/dco.yml
  • .github/dependabot.yml
  • .github/labeler.yml
  • .github/scripts/apply-settings.js
  • .github/scripts/bootstrap.py
  • .github/scripts/repo-lint.py
  • .github/settings.yml
  • .github/workflows/all-contributors.yml
  • .github/workflows/apply-settings.yml
  • .github/workflows/ci.yml
  • .github/workflows/codeql.yml
  • .github/workflows/dco.yml
  • .github/workflows/dependency-review.yml
  • .github/workflows/hygiene.yml
  • .github/workflows/labeler.yml
  • .github/workflows/license-check.yml
  • .github/workflows/pr-size-labeler.yml
  • .github/workflows/pr-title.yml
  • .github/workflows/publish-image.yml
  • .github/workflows/release-assets.yml
  • .github/workflows/release-please.yml
  • .github/workflows/scorecard.yml
  • .github/workflows/spellcheck.yml
  • .github/workflows/welcome.yml
  • .gitignore
  • .golangci.yaml
  • .yamllint
  • AGENTS.md
  • CHANGELOG.md
  • CHECKLIST.md
  • CLAUDE.md
  • CODE_OF_CONDUCT.md
  • CONTRIBUTING.md
  • CONTRIBUTORS.md
  • Dockerfile
  • LICENSE
  • MAINTAINERS.md
  • NOTICE
  • README.md
  • ROADMAP.md
  • SECURITY.md
  • SUPPORT.md
  • Taskfile.yml
  • current-settings.yml
  • docs/RELEASES.md
  • docs/repo-automation.md
  • go.mod
  • lefthook.yml
  • main.go
  • main_test.go
  • optional/renovate.json
  • release-please-config.json
  • versions.env

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Dependencies update component/ci CI/CD related component/config tests labels Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails
actions/actions/checkout 3d3c42e5aac5ba805825da76410c181273ba90b1 🟢 7
Details
CheckScoreReason
Maintained🟢 1024 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 10all changesets reviewed
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
Packaging⚠️ -1packaging workflow not detected
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Pinned-Dependencies🟢 3dependency not pinned by hash detected -- score normalized to 3
Security-Policy🟢 9security policy file detected
SAST🟢 10SAST tool is run on all commits
Branch-Protection🟢 6branch protection is not maximal on development and all release branches
actions/github/codeql-action/upload-sarif ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd UnknownUnknown
actions/ossf/scorecard-action 2d1146689b8cda280b9bc96326124645441f03bc 🟢 8.1
Details
CheckScoreReason
Code-Review⚠️ -1Found no human activity in the last 30 changesets
Dependency-Update-Tool🟢 10update tool detected
Security-Policy🟢 10security policy file detected
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Pinned-Dependencies🟢 9dependency not pinned by hash detected -- score normalized to 9
Maintained🟢 1027 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Vulnerabilities⚠️ 012 existing vulnerabilities detected
Packaging🟢 10packaging workflow detected
License🟢 10license file detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST🟢 10SAST tool is run on all commits
CI-Tests🟢 1030 out of 30 merged PRs checked by a CI test -- score normalized to 10
Contributors🟢 10project has 18 contributing companies or organizations

Scanned Files

  • .github/workflows/all-contributors.yml
  • .github/workflows/license-check.yml
  • .github/workflows/scorecard.yml
  • .github/workflows/spellcheck.yml

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread .github/dependabot.yml Fixed
Comment thread .github/dependabot.yml Fixed
Comment thread .github/dependabot.yml Fixed
Comment thread .github/workflows/apply-settings.yml Fixed
Comment thread .github/workflows/release-assets.yml Fixed

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your first pull request! 🎉

A maintainer will review this soon. Please ensure:

  • Tests pass locally
  • Code follows our style guidelines
  • Commits are signed off (DCO)
  • PR title follows Conventional Commits

Check out our Contributing Guide for more details.

@github-actions github-actions Bot added the size/XL Extra large PR (> 1000 lines) label Aug 21, 2026
…alled

CI failed Lint Go and License Check on the first run: golangci-lint and
go-licenses are not on the runner and nothing installed them, while
vuln-check passed because it already used go run with a pinned version.

All three now use the same pattern, with the version coming from
versions.env, so no tool has to be preinstalled and a local run uses
exactly the same version as CI. go-licenses is pinned at v2.0.1, whose
module path is /v2.

Dropped the _require-golangci-lint guard: it existed only to produce a
better message for a situation that can no longer happen.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Taskfile.yml
… and stop swallowing 403 on three more sections

Three review threads, all valid.

Release baseline (coderabbit, cubic P1, copilot). The repository's only
release is tagged app-v1.1.0, which the new vX.Y.Z scheme does not
match, so release-please would find no previous release, walk the whole
history and replay 27 old commits into the first changelog.
release-please-config.json now carries a one-time
bootstrap-sha at the current main tip. docs/RELEASES.md explains what
it is, says to delete it after the first release under the new scheme,
and gives the tag-based alternative for anyone who would rather keep
the 1.1.0 line. CHECKLIST tells adopters to remove the key, since it
points at a commit that does not exist in their repository.

Version impact (cubic P3). README claimed "Other types do not trigger a
release by themselves", which contradicted docs/RELEASES.md and
CHECKLIST. The README was the wrong one. Verified against
release-please rather than argued: src/versioning-strategies/default.ts
determineReleaseType returns PatchVersionUpdate() for any commit set
with no breaking change and no feat, and src/strategies/base.ts skips
only when conventionalCommits.length === 0, with postProcessCommits an
identity function in the base strategy. Nothing filters on commit type.
So every conventional commit not excluded by path produces at least a
patch release, and `hidden: true` only removes a section from the
changelog. All three tables now say the same thing, and the README says
to use exclude-paths if a change really should not release.

Drift false positives (cubic P2). The previous commit taught the
exporter that 403 is not the same as "not configured", but only for
actions and code_scanning. The rulesets, branches and environments
reads still turned any failure into an omitted section, so a token
without administration access reported permanent drift on exactly the
sections this was meant to protect. All five now go through
markIfForbidden.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Vad1mo added 2 commits August 21, 2026 13:15
…sion file

copilot raised this on both README.md and docs/RELEASES.md and it is
correct. Verified against release-please source:

- src/strategies/simple.ts: versionFile defaults to 'version.txt' and
  the updater is always registered.
- src/strategies/go.ts: versionFile defaults to '' and the updater is
  registered only `if (this.versionFile)`. Go modules are versioned by
  tags, so the strategy maintains the changelog and the tag and nothing
  else.

Listing `go` next to node/python/rust/helm as "switch to update your
native version file" therefore leads an adopter to switch, lose
version.txt with nothing replacing it, and not notice. It also breaks
the release-assets task, which stamps the binary from version.txt.

All three documents now say to stay on `simple` or set version-file
explicitly.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Brings in the PR #5 review fixes: the bootstrap-sha migration boundary,
markIfForbidden extended to rulesets/branches/environments, the
corrected version-impact claim, and the go release-type caveat.

README.md and CHECKLIST.md are taken from this branch, which rewrote
both. Neither carries the claims the base fixed; both now defer to
docs/RELEASES.md, where the corrections landed.

task bootstrap now strips bootstrap-sha. It is a migration boundary for
the template's own history and points at a commit that does not exist in
an adopter's repository, so leaving it as a checklist item nobody reads
was the wrong place for it.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread .github/workflows/ci.yml
Comment thread .github/scripts/bootstrap.py
Comment thread .github/scripts/apply-settings.js Outdated
Comment thread .github/scripts/repo-lint.py Outdated
Comment thread Taskfile.yml
Comment thread .github/workflows/scorecard.yml Outdated
Comment thread MAINTAINERS.md Outdated
Comment thread .editorconfig
Comment thread AGENTS.md Outdated
Comment thread main.go Outdated
Vad1mo added 2 commits August 21, 2026 13:30
… I committed

cubic was right on all four points in its re-review.

Release triggers. My previous commit claimed hidden changelog sections
affect only the changelog and that every conventional commit produces at
least a patch release. That is wrong, and cubic was right that hidden
types do not release. The mechanism is in release-please
src/strategies/base.ts:

    if (!bumpOnlyOptions && this.changelogEmpty(releaseNotesBody)) {
      ... - skipping
      return undefined;
    }

changelogEmpty is true when the generated entry is a single line, which
is what happens when every commit landed in a `hidden: true` section. So
the release is skipped outright rather than the bump being suppressed.

The complete rule, which neither the previous wording nor cubic's stated
it fully:

- only hidden types since the last release -> no release at all
- any visible commit -> a release is cut, minor for feat, major for a
  breaking change, patch for everything else
- so one `docs:` alongside five `chore:` commits is a patch release, and
  the changelog shows only the `docs:` line

That last case is why "docs/perf/refactor/revert do not trigger a
release" is also wrong: this repository configures them as visible
sections, unlike release-please's defaults. All three tables now say the
same thing and name the mechanism rather than the commit type.

coverage.out. Committed by my own `git add -A` in dbe6698: the file was
left in the working tree by a task run on another branch, and this
branch's .gitignore predates the entry that would have excluded it. It
is stale, nothing consumes it, and no test exists here to regenerate it.
Removed, and .gitignore now covers coverage and lint output.

Also moved the bootstrap-sha checklist item under
`release-please-config.json`, the file it actually edits, and split the
monorepo sentence back out of the `go` caveat paragraph, where an edit
had glued it onto a 270-character line.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Brings in the corrected release-trigger rule: hidden changelog sections
make release-please skip the release outright via changelogEmpty, rather
than the bump being suppressed. docs/RELEASES.md carries it; README.md
and CHECKLIST.md are taken from this branch, which rewrote both and
defers the release semantics to that document.

.gitignore taken from this branch, which is a superset and matches the
licenses.csv this branch's Taskfile actually writes.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
zizmor, via code scanning:
- dependabot: cooldown default-days 7 on all three ecosystems, so a
  version published minutes ago is not proposed automatically
- apply-settings: `npm ci` from a committed package-lock.json instead of
  an ad-hoc install, in the one job that can hold an administration
  token
- release-assets: `gh release upload` instead of a third-party action;
  gh is already on the runner and the job holds contents: write

Settings drift, all of which made verify and check fail permanently:
- exportSettings still returned visibility and is_template after both
  were deliberately removed from settings.yml, so expected and actual
  could never match
- description is null on a repo without one but "" in settings.yml, and
  normalize drops null while keeping ""
- labels were exported wholesale, so every label the repository has but
  settings.yml does not declare read as drift. release-please alone adds
  autorelease: pending and autorelease: tagged. Only declared labels are
  compared now, which is what settings.yml actually claims to manage
- applySecurity ran the PATCH enabling dependabot_security_updates
  before the PUT enabling vulnerability_alerts, its precondition, so the
  request was rejected and the whole section threw. The comment already
  said alerts go first; now the code does

Bootstrap:
- marker comments were only removed when the optional value was blank,
  so supplying a DOCS_URL left `<!-- if:DOCS_URL -->` behind and
  repo-lint rejected the adopted repo. The markers are now always
  removed and only the content is conditional. The rehearsal covered
  just the blank path, which is why this shipped: it now runs all four
  combinations of --lang and filled/blank optional values
- --module-path and --homepage-url were collected and never applied.
  Both are written explicitly rather than through a placeholder: a
  module path with braces is not a valid module path, and settings.yml
  is applied to the live repository, so a placeholder there would be
  written to the template's own homepage
- values containing a quote or backslash are rejected rather than
  silently producing an invalid Dockerfile LABEL
- --lang=none prunes the rows in docs/repo-automation.md that document
  the files it just deleted

Workflows:
- pr-title moves back to pull_request_target. With pull_request, a pull
  request that edits the workflow runs its own edited copy and can
  replace the check that gates it. The trigger is safe here only
  because nothing checks out the head, which repo-lint now enforces
- the cosign identity in the generated verify command pointed at
  publish-image.yml@refs/tags/<tag>. Fulcio records the reusable
  workflow at the ref it was resolved at, which for a local call is the
  caller's ref, so every generated command failed. Now an anchored
  identity regexp
- document-artifacts needs publish-release-assets: it was publishing
  instructions to verify files that job had not uploaded yet
- license-report ran under bare always(), so the no-Go path invoked
  task without having installed it
- codeql concurrency now includes the event name; a push could cancel
  the scheduled run that exists to catch new queries on old code
- the SBOM described the checkout rather than the artifacts, and was not
  covered by checksums.txt. It now scans dist, checksums run after it,
  and both the binaries and the SBOM are attested
- release-checksums wrote through a redirect that created the file
  before the glob expanded, so checksums.txt contained a checksum of
  its own empty self
- the Task download verified against a checksum file fetched from the
  same release, which only proves the release is self-consistent. The
  digests are committed in versions.env, as they already were for typos
  and actionlint
- SETTINGS_TOKEN needs Issues read/write; labels go through the Issues
  API and the documented scope omitted it
- dco accepted a bare "Signed-off-by:" line; it now requires a name and
  an address
- PyYAML is installed rather than assumed to be on the runner

repo-lint:
- the pull_request_target guard only understood the mapping trigger
  form, so `on: pull_request_target` skipped the check entirely
- composite actions may be action.yaml
- the post-bootstrap scan now covers the same files bootstrap rewrites,
  including Dockerfile, LICENSE, CODEOWNERS, go.mod and .gitignore
- new check: the Dockerfile base-image ARG defaults must match
  versions.env, which were two independent copies of the same pin

Smaller: task check no longer claims CI parity it does not have and now
runs build and the pin check; the README quick start points --template
at the template rather than at the repository being created;
MAINTAINERS explains that CODEOWNERS routes to a team, so adding a row
alone changes nothing; .editorconfig covers Python; AGENTS.md is a
pointer file rather than a symlink, which does not survive a default
Windows clone; main.go checks the write error in both branches; the
codeql pins carry their exact version; and docs/RELEASES.md says which
paths exclude-paths actually covers.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
@Vad1mo

Vad1mo commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Bot review addressed in c15e5c2. 38 of 43 findings were valid and are fixed; the five below I am not taking, with reasons.

ci.yml — "task license-check cannot find go-licenses" / "restore a pinned binary for golangci-lint"
Stale. Both tools are fetched with go run <module>@<version> from versions.env, so nothing needs preinstalling — that was changed in 132612b precisely because CI failed without it. Lint Go and License Check are green on this branch.

Taskfile.yml:120 — "bin/ does not exist, so go build -o bin/ ./... fails"
go build -o <dir>/ creates the directory. Verified: rm -rf bin && task build produces bin/oss-project-template, and the Build job is green.

Dockerfile:22 — "COPY go.mod go.su[m] ./ fails when there is no go.sum"
Docker only errors when no source pattern matches. go.mod always matches, so the optional go.su[m] glob resolving to nothing is fine — which is exactly the current state of this repository, and the Image job builds both architectures green. The glob is deliberate: it is what lets the same Dockerfile work with and without dependencies.

hygiene.yml:51 — "/usr/local/bin is root-owned, so extraction fails"
/usr/local/bin is writable by the runner user on GitHub-hosted runners. Spell Check and Lint Workflows both install there and pass.

.github/labeler.yml:17 — "taskfile/**, config/**, configs/** match nothing"
Correct that they match nothing today, and intended. A labeler config that only covers what an empty template ships stops being useful after the adopter's first commit; taskfile/ is the split-Taskfile layout used across this org, and config/ and configs/ are the two conventional names. The alternative — every adopter rediscovering that they need to add these — is worse than three inert globs.

One partial: ci.yml:153 is right that the header said vuln-check was weekly while the job also runs per-PR. Running it per-PR is deliberate — it is what catches a newly introduced vulnerable dependency — so I corrected the comment rather than the schedule.

Two of the valid findings are worth calling out because they were only reachable by reading interactions, not files: the settings exporter still returned visibility and is_template after both were removed from settings.yml, which would have made the weekly drift check fail forever; and bootstrap left its <!-- if:DOCS_URL --> markers behind whenever the value was actually supplied. The rehearsal missed the second one because it only ever ran with blank optional values — it now runs all four combinations.

Vad1mo added 3 commits August 21, 2026 13:45
coderabbit is right that a bare 'None' is imprecise. A push of only
hidden types cuts no release, but in a mixed push the release happens
and those commits ride along inside it, so the row overstated the case.
The prose below each table already described the mixed case; the row now
agrees with it.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Wording fix for hidden commit types. README.md and CHECKLIST.md taken
from this branch, which rewrote both and does not carry a commit-type
table; docs/RELEASES.md is where the rule lives.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Both failures on the last run were mine.

Dependency Review failed correctly. The lockfile I added to satisfy
zizmor's ad-hoc-install finding pinned js-yaml 4.1.0, which carries two
high-severity advisories (GHSA-52cp-r559-cp3m and GHSA-5p4m-2wfm-xmqj,
quadratic CPU on merge keys and on !!omap). Rather than pin a vulnerable
parser, the dependency is gone: the workflow converts settings.yml to
JSON with python3 before the script runs, and apply-settings.js parses
JSON. That removes the advisory, the lockfile, and the original zizmor
finding together, and keeps every third-party dependency out of the one
job that can hold an administration token.

PyYAML ships on GitHub's Ubuntu runners - the repo-lint job proved that
by passing before it had an install step - so the pip install is a
fallback rather than the normal path.

Bootstrap Rehearsal failed on `home: unbound variable`. The previous
commit added the four rehearse invocations and the "$home"/"$docs"
reference but not the parameter block that defines them, because that
half of the edit failed to apply and I only noticed the half that did.
Restored, and all four combinations pass locally.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Base automatically changed from agent/generic-release-please to main August 21, 2026 11:56

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread .github/workflows/apply-settings.yml Outdated
Comment thread .github/workflows/apply-settings.yml Outdated
Comment thread .github/workflows/hygiene.yml Outdated
Vad1mo added 2 commits August 21, 2026 13:59
PR #5 was squash-merged, so its content arrived on main as one new
commit while this branch carries the original ones. Every conflict is
that duplication: main is exactly the PR #5 tip plus one bot commit, and
that tip is already an ancestor here, so this branch is the superset in
each conflicting file. Verified before resolving, not assumed.

Two resolutions are decisions rather than mechanics:

CONTRIBUTORS.md stays deleted. main gained it again from
`docs: update contributors`, which is the all-contributors workflow
pushing to the default branch. This branch removes that whole stack -
CONTRIBUTORS.md, .all-contributorsrc and all-contributors.yml - because
it was three overlapping mechanisms for one job, none of them working,
one of which writes to main. That bot commit is the behaviour being
removed.

.github/scripts/apply-settings.js keeps the label filter from this
branch. main still exports every label the repository has, so any label
settings.yml does not declare reads as drift - including the
autorelease: pending and autorelease: tagged labels release-please
creates on its own release pull request.

bootstrap-sha stays at 9d06252, the last commit before the new release
scheme. It is still an ancestor of main and still the right boundary.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
cubic is right that an unpinned `pip install` in the job that can hold
an administration token is a supply-chain hole: it executes third-party
package code in the workspace before the github-script step runs, and
that code can rewrite apply-settings.js, which the next step loads with
SETTINGS_TOKEN.

The fallback also bought nothing. PyYAML is present on GitHub's Ubuntu
runners - the Repository Consistency job logs `Requirement already
satisfied: pyyaml ... (6.0.1)`. A runner without it now fails with a
message saying to fix the image rather than fetching a dependency into
a privileged job.

The install stays in the hygiene job, which holds no token and whose
whole purpose is running repo-lint, so a portable install there is worth
the far smaller risk.

Also corrected the rehearse() comment: $1 is the --lang value, not the
flag.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
@Vad1mo

Vad1mo commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

c15e5c2..latest addresses the three new threads. Two adopted, one I am not taking.

P1 on apply-settings.yml — adopted. Correct, and the fallback bought nothing anyway. Removed; the job now fails with a message pointing at the runner image rather than fetching a dependency into the one job that can hold an administration token. The install stays in the hygiene job, which holds no token and exists to run repo-lint.

P2 on the same line — I disagree. The premise is that pip 24 on ubuntu-latest's python3.12 refuses global installs with externally-managed-environment, making the fallback unrecoverable under set -euo pipefail. That is not what happens on these runners. From the Repository Consistency job on this branch:

Run pip install --disable-pip-version-check pyyaml
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: pyyaml in /usr/lib/python3/dist-packages (6.0.1)

pip falls back to a user install rather than refusing, and PyYAML is already there. The same is true of the pip install yamllint in two other jobs, which have been passing throughout.

This matters beyond the removed line: --break-system-packages should not be added to the remaining pip install calls on the strength of an error that does not occur here. If a future runner image does become externally managed, a venv is the fix, not that flag.

P3 — adopted, comment corrected.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread .github/workflows/apply-settings.yml Outdated
…lback

cubic is right that the comment above the step still described a
fallback the previous commit deleted, so an operator would have expected
the job to install PyYAML on a runner that lacks it. The two comments
also duplicated each other; they are now one, saying PyYAML is required
rather than fetched and why.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
@Vad1mo
Vad1mo merged commit 55cdd57 into main Aug 21, 2026
27 checks passed
@Vad1mo
Vad1mo deleted the feat/template-hardening branch August 21, 2026 12:31
@github-actions github-actions Bot mentioned this pull request Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/ci CI/CD related component/config dependencies Dependencies update documentation Improvements or additions to documentation size/XL Extra large PR (> 1000 lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants