Build a Secure OSS Repo Kit

Build a private GitHub kit with workflow policy, DCO checks, and a release.

Introduction

30 Second Summary

A reviewed change can behave differently later when one of its named building blocks points somewhere new. The contributor sees the same label while the code behind it has changed.

In this project, you will build a private open-source repository kit that applies security rules you control. You will use Open Policy Agent to test workflow safety before releasing the kit through one private pull request.

What You'll Build

Run your kit to watch its policy stop a planted workflow before the same private repository ships as release v0.1.0.

By the end of this project, you'll have:

  • Run deterministic checks that show safe workflows passing. See three planted workflows fail for exact reasons.
  • Catch an unsigned commit in your branch history. Remove it before it reaches the pull request.
  • Compare a local model's fix list with real policy findings. Record a measured score that cannot change the policy result.
  • Secret Mission: take on an optional challenge to push your repository policy skills further.

Are there any prerequisites?

You need a GitHub account that can create a private repository. The same account must be able to create a private Project.

The setup step checks every required tool before you create any project files.

Before We Start

Before the clock starts, commit to keeping the repository and GitHub Project private until the project stops at minute 60. Decision authority stays with your policy and its tests, with measured qwen3:1.7b output limited to explaining one core risk: mutable GitHub Action tags can move, while a full commit SHA selects one immutable Git object without proving that code is safe.

Verify Tools and Create the Local Repository

Your policy as code needs a known toolchain. It also needs a known contributor identity.

This step checks each local dependency. It also creates the empty repository that holds your design in the next step.

In this step, get ready to:
  • Verify Go, Git, GitHub CLI, Ollama, and the local model.
  • Confirm the contributor identity and private GitHub access.
  • Create an empty repository on main.
Verify the contributor toolchain

The project requires Go 1.26 or newer. Open Policy Agent v1.21.4 declares Go 1.26.0 as its module version.

  • In Cursor or Visual Studio Code, create a built-in terminal.
  • Check the installed Go version by running this command:
go version

What does this command check?

The command prints the Go runtime available in this terminal. The version determines whether the project can use the required OPA tool.

✔️ I see the required version

Your output shows Go 1.26 or newer. Your Go runtime is ready for the project.

ⓧ I see an older version

Your current Go version cannot run this project. Upgrade it before you continue.

  • Open the official Go downloads page.
  • Download the Windows MSI for Go 1.26 or newer.
  • Run the downloaded installer.
  • Close the current built-in terminal.
  • Create a new built-in terminal.
  • Verify the upgraded version by running this command:
go version

Why create a new terminal?

The installer updates the environment used by new terminal sessions. A new terminal loads the updated Go installation.

You should now see Go 1.26 or newer.

ⓧ Command not found

Windows cannot find Go in the current terminal. Install the required version before you continue.

  • Open the official Go installation guide.
  • Download the Windows MSI for Go 1.26 or newer.
  • Run the downloaded installer.
  • Close the current built-in terminal.
  • Create a new built-in terminal.
  • Verify the installation by running this command:
go version

What proves the installation worked?

The command should print a Go version. Confirm that the reported version is 1.26 or newer.

Git places your configured name and email in each signed-off commit. These values identify you as the contributor.

  • Check the configured contributor identity by running these commands:
git config user.name
git config user.email

What do these checks prove?

  • The first command prints the contributor name Git uses.
  • The second command prints the contributor email Git uses.

You should see your contributor name on the first line. You should see your contributor email on the second line.

Is an identity value missing?

  • Open the official Git tutorial.
  • Configure the missing identity value with the name or email you use for contributions.
  • Rerun the two identity checks above.

Help me configure a missing Git contributor identity value.

  • Check the current GitHub CLI authentication by running this command:
gh auth status

What does this authentication check show?

The output identifies the authenticated GitHub account. It also reports the access attached to the local GitHub CLI login.

✔️ My account is authenticated

GitHub CLI reports an authenticated account with private repository access. That account is ready for the private repository created later.

ⓧ I am not authenticated

You control this sign-in step. GitHub CLI stores the resulting authentication outside your project repository.

  • Use the GitHub account that can create private repositories when the interactive flow asks you to authenticate.
  • Start the interactive sign-in flow and recheck access by running these commands:
gh auth login
gh auth status

What do these commands do?

  • The first command starts GitHub CLI's interactive authentication flow.
  • The second command confirms that the chosen account is authenticated.

You should now see the authenticated GitHub account with private repository access.

You stay in control here. The next approval adds GitHub Projects access to your existing GitHub CLI login.

  • Add the required project scope by running this command:
gh auth refresh -s project

Why add this scope?

The project scope lets GitHub CLI work with GitHub Projects. You use that access when the private Project is created later.

GitHub CLI may open a browser approval page. After approval, the terminal confirms that the authentication scopes were refreshed.

Did the scope refresh fail?

Confirm that the authenticated account can create a private Project. Rerun the scope command after completing any browser approval.

Help me resolve a GitHub CLI project scope problem.

Verify the local model

The local model qwen3:1.7b drafts plain-language fixes later. Deterministic code measures those suggestions.

  • Check the models available to Ollama by running this command:
ollama list

What does this command check?

Ollama lists the models already stored on your computer. The project needs the exact qwen3:1.7b model identifier.

✔️ qwen3:1.7b is listed

The required local model is ready. Later commands can call it without sending project findings to a hosted model.

ⓧ qwen3:1.7b is absent

The model download can take several minutes. A few quiet moments are normal while Ollama downloads and verifies the model.

  • Download the required local model by running this command:
ollama pull qwen3:1.7b

What does this command do?

Ollama downloads the exact model used by this project. It also verifies the downloaded model before making it available locally.

You should see the pull finish successfully. The local qwen3:1.7b model is now ready.

Did the model download fail?

Confirm that Ollama is running locally. Retry the pull after checking your network connection.

Help me troubleshoot the local model download.

Create the empty local repository

The repository starts empty so the design documents can form the first commit alone. This preserves the project's design-first history.

  • Move the terminal to your home folder by running this command:
cd ~

What does this command do?

The ~ symbol refers to your home folder. Starting there creates the repository at the required path.

  • Create the oss-repo-kit repository on main by running this command:
git init -b main oss-repo-kit

What does this command create?

  • Git creates the oss-repo-kit folder in your home folder.
  • The -b main option names the initial branch main.
  • The repository begins without files or commits.

You should see confirmation that Git initialized an empty repository. The local foundation is now in place.

Was the repository already present?

Stop before deleting or overwriting anything. Confirm that ~/oss-repo-kit does not contain work you need to keep.

Help me inspect an existing repository safely.

  • Click File in your editor's top menu.
  • Click Open Folder.
  • Select the ~/oss-repo-kit folder.
  • Click Open.
  • Create a new built-in terminal in the opened folder.
  • Set the terminal path explicitly by running this command:
cd ~/oss-repo-kit

Why set the path explicitly?

The command places the terminal inside the repository you just created. Your terminal path should now end in oss-repo-kit.

Before you run the final check, what should an untouched repository on main report?

  • Verify the branch and empty working tree by running this command:
git status --short --branch

What does this final check prove?

  • The branch summary confirms that the repository is on main.
  • The missing file entries confirm that the working tree is empty.
  • The branch message confirms that no commits exist.

You should see ## No commits yet on main with no file entries below it. That output proves the repository matches the required starting state.

✔️ Awesome, I've got everything!

Your local repository matches the checkpoint. It is empty on main with no commits.

ⓧ I'd like to double check the full code

Compare your repository against this exact checkpoint.

Initialized with `git init -b main`; branch is `main`, working tree is empty, and no commits exist.

How to read this checkpoint

  • The repository was initialized with the required command.
  • The current branch is main.
  • The working tree contains no files.
  • The repository contains no commits.

Your verified toolchain and empty repository are ready. Next, you will commit the five design documents before writing any code.

Design the Private Repository Kit

Your verified tools now sit behind an empty Git repository. This step turns that blank history into a reviewable contributor kit.

Design must be reviewable before implementation begins. You will preserve that boundary by committing the five design documents alone before adding the starter code.

In this step, get ready to:
  • Commit the five design documents alone as the first signed-off commit.
  • Add the complete starter kit as a separate signed-off commit.
  • Create the private repository, Project, issue, and policy branch.
Commit the design before code

The design documents define the scope, decisions, topology, implementation plan, and tests. Their isolated commit creates an auditable record of what the code must achieve.

What is policy as code?

Policy as code stores rules in source files that can be reviewed and tested. Your design records the expected policy behavior before you write those rules.

The first commit also uses a DCO sign-off. This trailer certifies your contribution in the commit message.

  • Return to the Explorer sidebar in your editor.
  • Create the docs folder inside oss-repo-kit with the New Folder control.
  • Create docs/build-spec.md with the New File control.
  • Create docs/decisions.md with the New File control.
  • Create docs/topology.md with the New File control.
  • Create docs/implementation-plan.md with the New File control.
  • Create docs/test-plan.md with the New File control.
  • Use the full-file references below to fill each matching design file.

✔️ Awesome, I've got everything!

Confirm that the docs folder contains all five saved design files.

ⓧ I'd like to double check the full code

# Build Specification

## The ask

Build one private `oss-repo-kit` at 0.00 USD. Use one local model, `qwen3:1.7b`. Prove pinned action references, read-only top-level permissions, blocked `pull_request_target`, DCO sign-offs, one pull request, and one release.

## The problem

On 2026-03-19, a threat actor used compromised credentials to force-push 76 of 77 version tags in `aquasecurity/trivy-action` to malicious commits. Source: Aqua Security advisory GHSA-69fq-xp46-6x23, published 2026-03-21.

A mutable tag can move. A full commit SHA keeps a workflow on one Git object. This does not prove the selected code is safe. It does not fix actions that other projects still name by tag.

## Acceptance criteria

- AC-1: Repository and Project remain private.
- AC-2: Five design documents are the first commit, alone, before code.
- AC-3: The page contains the three Go commands, starter policy, twelve tests, four fixtures, CI, Dependabot, ignore files, kit, README skeleton, limits, upstream draft, and after-action review.
- AC-4: The contributor writes `policy/workflow.rego`; the page supplies no completed policy.
- AC-5: Tests report `PASS: 12/12`; safe workflows print two `PASS` lines.
- AC-6: Three planted workflows print three `FAIL` lines and exit 1.
- AC-7: The unsigned planted commit prints `MISSING` and exits 1; reset removes it; the next run prints only `OK`.
- AC-8: The measured fix list records a score out of 3 and the exact struck item observation; one pull request merges and release `v0.1.0` exists.

## Out of scope

- Full OpenSSF Scorecard coverage.
- Job-level permission rules.
- Making the repository or Project public.
- Posting the upstream draft.
- Letting a model decide policy.

What does the build specification define?

The build specification fixes the problem, acceptance criteria, and exclusions. Reviewers can use it to judge the finished kit.

# Decisions

## My decisions

### Decision 1: Use OPA in Go instead of the Scorecard Action

Choice: Run the repository's own tested Rego with OPA in Go.

Alternative: Run the OpenSSF Scorecard Action.

Cost: The local policy covers only the named rules. It is not Scorecard.

Reversal trigger: A separate future project with different approved cost and privacy constraints.

### Decision 2: The model explains; the policy decides

Choice: Let `qwen3:1.7b` draft plain-language fixes. Let policy tests, findings, and deterministic matching decide every result.

Alternative: Let model output classify or waive findings.

Cost: A useful explanation can receive no credit when it misses the measured rule.

Reversal trigger: None. The model never becomes policy authority.

What do the decisions record?

Each decision records a choice, alternative, cost, and reversal trigger. Keeping both under My decisions makes your engineering ownership explicit.

# Topology

```text
workflow files and fixtures -> wfcheck -> your policy -> findings.json
                                                      |
                                                      v
                                                fixlist scorer
                                                      |
                                                      v
                                                qwen3:1.7b

Git commits --------------------------------------> signoff

issue 1 -> workflow-policy -> one pull request -> main -> v0.1.0
                         all inside one private repository
                         tracked in one private Project
```

Trust boundary: workflow YAML and model text are input. Your policy, its tests, Git history, and deterministic matching decide.

What does the topology show?

The topology separates workflow and model input from the deterministic decision points. It also maps the contribution path from issue 1 to the release.

# Implementation Plan

| Phase | Minutes | Finish state |
| --- | ---: | --- |
| Setup | 10 | Tools, identity, access, model, and empty repository checked |
| Design | 8 | Five design files committed alone before code; starter kit pushed privately |
| Build | 20 | Contributor policy and two decisions committed; 12 tests pass |
| Validation | 12 | Pass, fail, sign-off, reset, and measured model results pasted into the test plan |
| Close-out | 10 | One pull request merged; one private release made; upstream draft not posted |

Total: 60 minutes. Stop at minute 60 and record the current step.

What does the plan control?

The implementation plan assigns a timebox and finish state to every phase. It preserves the fixed minute-60 stopping boundary.

# Test Plan

Paste each observed result in the last column during Validation. Do not replace an observation with an estimate.

| Check | Required result | Observed result |
| --- | --- | --- |
| Policy tests | `PASS: 12/12` | Paste the summary here. |
| Safe workflow check | Two `PASS` lines | Paste both lines here. |
| Three planted workflows | Three `FAIL` lines and exit status 1 | Paste all three lines and the exit status here. |
| Planted unsigned commit | `MISSING` and exit status 1 | Paste the `MISSING` line and exit status here. |
| Sign-off after reset | Only `OK` | Paste every `OK` line here. |
| Fix-list measurement | `Measured:` score out of 3; struck item named | Paste the `Measured:` line and exact struck `NO MATCH` item. If none was struck, write `No struck item observed`. |

What does the test plan preserve?

The table separates required outcomes from observed evidence. You will fill its last column during validation.

  • Save all five design files with Ctrl+S.
  • Review every line under My decisions in docs/decisions.md.
  • Confirm that the Explorer sidebar shows exactly five files inside docs.
  • Stage only the five design files by running this command:
git add docs/build-spec.md docs/decisions.md docs/topology.md docs/implementation-plan.md docs/test-plan.md

What does this command stage?

This command stages only the five named design files. No implementation file can enter the first commit through this command.

  • Check the Source Control panel for the five staged design files.
  • Create the first signed-off commit by running:
git commit -s -m "Add the design docs"

What does the sign-off add?

The -s option adds a Signed-off-by trailer from your configured contributor identity. The trailer is a contribution certification.

That is the design boundary secured. Your repository history now begins with intent before implementation.

Before you check the history, predict which subject should appear as the only commit.

  • Verify the first commit subject by running:
git log --reverse --format=%s

What should you see?

You should see one line containing Add the design docs. This proves the design commit exists before any starter code.

Is the first commit incorrect?

Check the Source Control panel for unstaged design files. A remaining file was excluded from the commit.

Use Help me inspect my first design commit.

Build and verify the starter kit

The first commit now preserves the design by itself. The second commit can add the implementation scaffold without changing that historical boundary.

The starter uses Go for three commands. It records Open Policy Agent version 1.21.4 as a module tool so local checks and continuous integration use the same runtime.

Why OPA in Go?

The repository needs a private path with a narrowly scoped policy that you own. Recording OPA in the Go module keeps the policy runtime version beside the code.

OpenSSF Scorecard covers a broader set of checks. This kit deliberately implements only the named workflow rules.

  • Create the Go module by running:
go mod init oss-repo-kit

What does this command create?

This command creates go.mod with module name oss-repo-kit. The module becomes the dependency boundary for the three commands.

  • Record OPA as a Go tool by running:
go get -tool github.com/open-policy-agent/opa@v1.21.4

What does this command record?

The command records OPA v1.21.4 in the module tool configuration. Later checks can invoke that exact runtime through go tool opa.

Could not record OPA?

Confirm that the built-in terminal still points to oss-repo-kit. A network failure can also interrupt the dependency download.

Use Help me diagnose the OPA tool setup.

  • Create cmd/wfcheck/main.go with the New File control in the Explorer sidebar.
  • Create cmd/signoff/main.go with the New File control.
  • Create cmd/fixlist/main.go with the New File control.
  • Create policy/workflow.rego with the New File control.
  • Create policy/workflow_test.rego with the New File control.
  • Create fixtures/good/release.yml with the New File control.
  • Create .github/workflows/ci.yml with the New File control.

How is the starter divided?

The three command folders separate workflow checking, sign-off checking, and fix-list measurement. The policy and fixture folders keep deterministic rules beside their test inputs.

  • Create .github/dependabot.yml with the New File control.
  • Create .gitignore with the New File control.
  • Create .gitattributes with the New File control.
  • Create CONTRIBUTING.md with the New File control.
  • Create SECURITY.md with the New File control.
  • Create CODE_OF_CONDUCT.md with the New File control.
  • Create README.md with the New File control.
  • Use the full-file references below to fill each matching starter file.

✔️ Awesome, I've got everything!

Confirm that every starter file is saved before resolving the module dependencies.

ⓧ I'd like to double check the full code

package main

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/open-policy-agent/opa/v1/rego"
	"go.yaml.in/yaml/v3"
)

type finding struct {
	File    string `json:"file"`
	Rule    string `json:"rule"`
	Message string `json:"message"`
}

func main() { os.Exit(run()) }

func run() int {
	dumpFile := flag.String("dump", "", "print one workflow as plain JSON")
	writeJSON := flag.Bool("json", false, "write findings.json")
	flag.Parse()
	if *dumpFile != "" {
		if flag.NArg() != 0 { return usageError("-dump takes one file and no folder arguments") }
		value, err := readWorkflow(*dumpFile)
		if err != nil { return commandError(err) }
		encoded, err := json.MarshalIndent(value, "", "  ")
		if err != nil { return commandError(err) }
		fmt.Println(string(encoded))
		return 0
	}
	if flag.NArg() == 0 { return usageError("name at least one workflow file or folder") }
	files, err := collectWorkflowFiles(flag.Args())
	if err != nil { return commandError(err) }
	if len(files) == 0 { return commandError(errors.New("no .yml or .yaml files found")) }
	ctx := context.Background()
	query, err := rego.New(rego.Query("data.workflow.deny"), rego.Load([]string{"policy"}, nil)).PrepareForEval(ctx)
	if err != nil { return commandError(fmt.Errorf("prepare policy: %w", err)) }
	allFindings := make([]finding, 0)
	failed := false
	for _, file := range files {
		value, err := readWorkflow(file)
		if err != nil { return commandError(err) }
		messages, err := evaluate(ctx, query, value)
		if err != nil { return commandError(fmt.Errorf("evaluate %s: %w", slash(file), err)) }
		if len(messages) == 0 { fmt.Printf("PASS %s\n", slash(file)); continue }
		failed = true
		for _, message := range messages {
			item := finding{File: slash(file), Rule: ruleName(message), Message: message}
			allFindings = append(allFindings, item)
			fmt.Printf("FAIL %s: %s\n", item.File, item.Message)
		}
	}
	if *writeJSON {
		encoded, err := json.MarshalIndent(allFindings, "", "  ")
		if err != nil { return commandError(err) }
		encoded = append(encoded, '\n')
		if err := os.WriteFile("findings.json", encoded, 0o644); err != nil { return commandError(err) }
		fmt.Printf("wrote findings.json (%d findings)\n", len(allFindings))
	}
	if failed { return 1 }
	return 0
}

func collectWorkflowFiles(paths []string) ([]string, error) {
	seen := map[string]bool{}
	var files []string
	for _, path := range paths {
		info, err := os.Stat(path)
		if err != nil { return nil, fmt.Errorf("inspect %s: %w", path, err) }
		if !info.IsDir() {
			if isWorkflowFile(path) && !seen[path] { seen[path] = true; files = append(files, path) }
			continue
		}
		err = filepath.WalkDir(path, func(current string, entry fs.DirEntry, walkErr error) error {
			if walkErr != nil { return walkErr }
			if entry.IsDir() || !isWorkflowFile(current) || seen[current] { return nil }
			seen[current] = true
			files = append(files, current)
			return nil
		})
		if err != nil { return nil, fmt.Errorf("walk %s: %w", path, err) }
	}
	sort.Slice(files, func(i, j int) bool { return slash(files[i]) < slash(files[j]) })
	return files, nil
}

func isWorkflowFile(path string) bool { ext := strings.ToLower(filepath.Ext(path)); return ext == ".yml" || ext == ".yaml" }

func readWorkflow(path string) (any, error) {
	data, err := os.ReadFile(path)
	if err != nil { return nil, fmt.Errorf("read %s: %w", slash(path), err) }
	var decoded any
	if err := yaml.Unmarshal(data, &decoded); err != nil { return nil, fmt.Errorf("parse %s: %w", slash(path), err) }
	encoded, err := json.Marshal(decoded)
	if err != nil { return nil, fmt.Errorf("convert %s to JSON: %w", slash(path), err) }
	var plain any
	if err := json.Unmarshal(encoded, &plain); err != nil { return nil, fmt.Errorf("normalize %s: %w", slash(path), err) }
	return plain, nil
}

func evaluate(ctx context.Context, query rego.PreparedEvalQuery, input any) ([]string, error) {
	results, err := query.Eval(ctx, rego.EvalInput(input), rego.EvalSortSets(true))
	if err != nil { return nil, err }
	if len(results) == 0 || len(results[0].Expressions) == 0 { return nil, nil }
	values, ok := results[0].Expressions[0].Value.([]any)
	if !ok { return nil, errors.New("data.workflow.deny must be a set of strings") }
	messages := make([]string, 0, len(values))
	for _, value := range values {
		message, ok := value.(string)
		if !ok { return nil, errors.New("data.workflow.deny contains a non-string value") }
		messages = append(messages, message)
	}
	sort.Strings(messages)
	return messages, nil
}

func ruleName(message string) string { before, _, found := strings.Cut(message, ":"); if !found { return "" }; return strings.TrimSpace(before) }
func slash(path string) string { return filepath.ToSlash(filepath.Clean(path)) }
func usageError(message string) int { fmt.Fprintln(os.Stderr, message); fmt.Fprintln(os.Stderr, "usage: wfcheck [-dump FILE] [-json] FILE_OR_FOLDER..."); return 2 }
func commandError(err error) int { fmt.Fprintln(os.Stderr, "error:", err); return 2 }

What does wfcheck do?

The command loads workflow YAML and evaluates data.workflow.deny against normalized input. It prints deterministic pass or fail lines.

The dump path exposes parsed workflow data. The JSON path writes structured findings for later measurement.

package main

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strings"
)

var signoff = regexp.MustCompile(`(?m)^Signed-off-by: [^<>\r\n]+ <[^<>\s]+@[^<>\s]+>$`)

func main() { os.Exit(run()) }

func run() int {
	if len(os.Args) != 2 { fmt.Fprintln(os.Stderr, "usage: signoff REVISION_RANGE"); return 2 }
	command := exec.Command("git", "log", "--abbrev=7", "--format=%h%x1f%s%x1f%B%x1e", os.Args[1])
	output, err := command.Output()
	if err != nil { fmt.Fprintln(os.Stderr, "error: git log:", err); return 2 }
	missing := 0
	for _, record := range bytes.Split(output, []byte{0x1e}) {
		record = bytes.TrimSpace(record)
		if len(record) == 0 { continue }
		fields := bytes.SplitN(record, []byte{0x1f}, 3)
		if len(fields) != 3 { fmt.Fprintln(os.Stderr, "error: unexpected git log record"); return 2 }
		hash := strings.TrimSpace(string(fields[0]))
		subject := strings.TrimSpace(string(fields[1]))
		body := string(fields[2])
		if signoff.MatchString(body) { fmt.Printf("OK %s %s\n", hash, subject); continue }
		fmt.Printf("MISSING %s %s\n", hash, subject)
		missing++
	}
	if missing > 0 { fmt.Printf("%d commit(s) without a sign-off; amend with git commit --amend -s\n", missing); return 1 }
	return 0
}

What does signoff do?

The command reads every commit in one Git revision range. Its regular expression checks each commit body for a complete Signed-off-by trailer.

It reports each commit as OK or MISSING. A missing trailer produces a failing exit status.

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

const endpoint = "http://127.0.0.1:11434/api/chat"
const model = "qwen3:1.7b"

type finding struct { File string `json:"file"`; Rule string `json:"rule"`; Message string `json:"message"` }
type item struct { File string `json:"file"`; Rule string `json:"rule"`; Fix string `json:"fix"` }
type itemList struct { Items []item `json:"items"` }
type chatMessage struct { Role string `json:"role"`; Content string `json:"content"` }
type chatRequest struct { Model string `json:"model"`; Messages []chatMessage `json:"messages"`; Stream bool `json:"stream"`; Think bool `json:"think"`; Format map[string]any `json:"format"`; Options map[string]any `json:"options"` }
type chatResponse struct { Message chatMessage `json:"message"` }

var fixPatterns = map[string]*regexp.Regexp{
	"unpinned": regexp.MustCompile(`(?i)\bsha\b|\bcommit\s+hash\b`),
	"permissions": regexp.MustCompile(`(?i)\bpermissions\b`),
	"trigger": regexp.MustCompile(`(?i)\bpull_request\b`),
}

func main() { os.Exit(run()) }

func run() int {
	data, err := os.ReadFile("findings.json")
	if err != nil { return commandError(fmt.Errorf("read findings.json: %w", err)) }
	var findings []finding
	if err := json.Unmarshal(data, &findings); err != nil { return commandError(fmt.Errorf("parse findings.json: %w", err)) }
	prompt := "Return one plain-words fix per finding. Copy each file and rule exactly. Do not decide whether a finding is valid. Findings JSON:\n" + string(data)
	requestBody := chatRequest{Model: model, Messages: []chatMessage{{Role: "user", Content: prompt}}, Stream: false, Think: false, Format: outputSchema(), Options: map[string]any{"temperature": 0}}
	encoded, err := json.Marshal(requestBody)
	if err != nil { return commandError(err) }
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(encoded))
	if err != nil { return commandError(err) }
	request.Header.Set("Content-Type", "application/json")
	response, err := http.DefaultClient.Do(request)
	if err != nil { return commandError(fmt.Errorf("call Ollama: %w", err)) }
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(response.Body, 4096)); return commandError(fmt.Errorf("Ollama returned %s: %s", response.Status, body)) }
	var chat chatResponse
	if err := json.NewDecoder(response.Body).Decode(&chat); err != nil { return commandError(fmt.Errorf("decode Ollama response: %w", err)) }
	var proposed itemList
	if err := json.Unmarshal([]byte(chat.Message.Content), &proposed); err != nil { return commandError(fmt.Errorf("decode structured content: %w", err)) }
	used := make([]bool, len(findings))
	matchedItems, coveredFindings := 0, 0
	var lines []string
	for _, proposedItem := range proposed.Items {
		index := matchingFinding(proposedItem, findings, used)
		label := fmt.Sprintf("%s [%s]: %s", slash(proposedItem.File), proposedItem.Rule, proposedItem.Fix)
		if index >= 0 { used[index] = true; matchedItems++; coveredFindings++; line := "- " + label + " - matches a finding"; fmt.Println(line); lines = append(lines, line); continue }
		line := "- ~~" + label + "~~ - NO MATCH"; fmt.Println(line); lines = append(lines, line)
	}
	measurement := fmt.Sprintf("Measured: %d of %d findings covered; %d of %d items matched", coveredFindings, len(findings), matchedItems, len(proposed.Items))
	fmt.Println(measurement)
	lines = append(lines, "", measurement)
	if err := os.WriteFile("fixlist.md", []byte("# Fix List\n\n"+strings.Join(lines, "\n")+"\n"), 0o644); err != nil { return commandError(err) }
	fmt.Println("wrote fixlist.md")
	return 0
}

func matchingFinding(proposed item, findings []finding, used []bool) int {
	file := slash(proposed.File)
	rule := strings.ToLower(strings.TrimSpace(proposed.Rule))
	pattern := fixPatterns[rule]
	if pattern == nil || !pattern.MatchString(proposed.Fix) { return -1 }
	for index, actual := range findings { if !used[index] && file == slash(actual.File) && rule == strings.ToLower(actual.Rule) { return index } }
	return -1
}

func outputSchema() map[string]any {
	return map[string]any{"type": "object", "additionalProperties": false, "required": []string{"items"}, "properties": map[string]any{"items": map[string]any{"type": "array", "items": map[string]any{"type": "object", "additionalProperties": false, "required": []string{"file", "rule", "fix"}, "properties": map[string]any{"file": map[string]any{"type": "string"}, "rule": map[string]any{"type": "string"}, "fix": map[string]any{"type": "string"}}}}}}
}

func slash(path string) string { return filepath.ToSlash(filepath.Clean(path)) }
func commandError(err error) int { fmt.Fprintln(os.Stderr, "error:", err); return 2 }

What does fixlist do?

The command sends findings to local Ollama using model qwen3:1.7b. Structured output limits the response shape.

Deterministic regular expressions measure each proposed fix against a real finding. Model text never changes a policy result.

package workflow

deny contains "example: this rule never fires" if {
	false
}

What does the starter policy do?

The starter preserves package workflow and set deny. Its only rule never fires, so the policy implementation remains your work.

package workflow_test

import data.workflow.deny

sha := "3d3c42e5aac5ba805825da76410c181273ba90b1"

good := {
	"on": {"pull_request": null},
	"permissions": {"contents": "read"},
	"jobs": {"test": {"steps": [
		{"uses": sprintf("actions/checkout@%s", [sha])},
		{"run": "go test ./..."},
	]}},
}

with_action(action) := object.union(good, {"jobs": {"test": {"steps": [{"uses": action}, {"run": "go test ./..."}]}}})
with_permissions(value) := object.union(good, {"permissions": value})
with_trigger(value) := object.union(good, {"on": value})

has_prefix(document, prefix) if {
	findings := deny with input as document
	some message in findings
	startswith(message, prefix)
}

test_good_passes if {
	findings := deny with input as good
	count(findings) == 0
}

test_tag_v4_fails_unpinned if { has_prefix(with_action("actions/checkout@v4"), "unpinned:") }
test_short_sha_fails_unpinned if { has_prefix(with_action("actions/checkout@3d3c42e"), "unpinned:") }
test_local_action_passes if { not has_prefix(with_action("./.github/actions/setup"), "unpinned:") }
test_no_permissions_fails if { has_prefix(object.remove(good, {"permissions"}), "permissions:") }
test_contents_write_fails if { has_prefix(with_permissions({"contents": "write"}), "permissions:") }
test_write_all_fails if { has_prefix(with_permissions("write-all"), "permissions:") }
test_read_all_passes if { not has_prefix(with_permissions("read-all"), "permissions:") }
test_on_string_fails_trigger if { has_prefix(with_trigger("pull_request_target"), "trigger:") }
test_on_list_fails_trigger if { has_prefix(with_trigger(["push", "pull_request_target"]), "trigger:") }
test_on_key_with_types_fails_trigger if { has_prefix(with_trigger({"pull_request_target": {"types": ["opened"]}}), "trigger:") }
test_on_key_with_null_fails_trigger if { has_prefix(with_trigger({"pull_request_target": null}), "trigger:") }

What do the twelve tests cover?

The suite covers immutable action references, local actions, top-level permissions, and four trigger shapes. Nine cases require rules that the starter policy does not contain.

Those failing cases define your next implementation target without supplying the completed policy.

name: release
on:
  push:
    tags:
      - 'v*'
permissions: read-all
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
      - run: go test ./...

What does the safe fixture represent?

The release fixture uses read-only permissions and a full action commit SHA. It provides a known-safe workflow for later policy checks.

name: policy

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  policy:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0
      - name: Set up Go
        uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
        with:
          go-version: '1.26'
          cache: false
      - name: Vet Go
        run: go vet ./...
      - name: Test policy
        run: go tool opa test policy
      - name: Check safe workflows
        run: go run ./cmd/wfcheck .github/workflows fixtures/good
      - name: Check pull request sign-offs
        if: github.event_name == 'pull_request'
        run: go run ./cmd/signoff ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}

What does the CI workflow enforce?

The GitHub Actions workflow grants read-only repository contents access. It pins checkout and Go setup to full commit SHAs.

The job vets Go, tests policy, checks safe workflows, and inspects pull request sign-offs.

version: 2
updates:
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
  - package-ecosystem: gomod
    directory: /
    schedule:
      interval: weekly

What does Dependabot monitor?

Dependabot checks GitHub Actions and Go module dependencies every week. Its proposals remain reviewable changes.

findings.json
fixlist.md

Why ignore these files?

The two files are generated evidence. Ignoring them keeps temporary findings and model output outside the committed source tree.

* text=auto eol=lf

What does this setting control?

This setting normalizes repository text files to line feed endings. It keeps diffs consistent between Windows and hosted runners.

# Contributing

Work as a contributor. Keep changes small and reviewable.

Every commit must include a DCO sign-off. Use `git commit -s`. The trailer certifies your contribution. It is not a cryptographic signature.

Run these checks before a pull request:

```text
go vet ./...
```

```text
go tool opa test policy
```

AI help must be disclosed in the pull request. Review every line. The contributor owns the result. The model decides nothing.

Keep the repository and Project private. Do not post `docs/upstream.md` from this project.

What does the contribution guide require?

The guide requires small changes, DCO sign-offs, local checks, AI disclosure, and line-by-line review. It also preserves the private and unposted boundaries.

# Security Policy

This repository is private. Report a suspected vulnerability to the repository owner through an approved private channel. Do not place vulnerability details in an issue, fixture, commit, workflow log, or model prompt.

Do not commit credentials, tokens, private data, or live exploit payloads.

What does the security policy protect?

The policy keeps vulnerability reports in approved private channels. It excludes credentials, private data, and live exploit payloads from repository artifacts.

# Community Code of Conduct

We want a respectful, open, and welcoming contributor space.

Use welcoming language. Respect different views and experiences. Give constructive feedback. Accept responsibility and correct mistakes.

Do not harass, threaten, insult, stalk, publish private information, use sexualized language or imagery, make unwelcome advances, retaliate against a reporter, or provide false information during an investigation.

Project maintainers may remove contributions or participation that violate these standards.

Report conduct concerns to the repository owner through an approved private channel.

What does the conduct policy establish?

The conduct policy defines expected contributor behavior and prohibited conduct. It provides a private reporting route through the repository owner.

# The Open-Source Repo Kit: Pinned Actions, Read-Only Tokens and Signed Commits, Proven by Your Own Policy

Status: Building on `workflow-policy`.

System used: Windows with PowerShell in the editor's built-in terminal.

## Problem

On 2026-03-19, 76 of 77 `aquasecurity/trivy-action` version tags were force-pushed to malicious commits. Source: Aqua Security advisory GHSA-69fq-xp46-6x23, published 2026-03-21.

## Evidence

Results are recorded in `docs/test-plan.md`.

## Limits

See `docs/limits.md` after close-out.

What does the README record?

The starter README records the working branch, Windows environment, incident context, and evidence location. Its limits section points to the future close-out document.

  • Save every starter file with Ctrl+S.
  • Confirm that the Explorer sidebar shows the three command folders, policy files, safe fixture, workflow files, and contributor documents.
  • Resolve the module dependencies by running:
go mod tidy

What does this command generate?

The command resolves imports from the three Go programs. It updates go.mod and generates go.sum with dependency checksums.

Did dependency resolution fail?

Compare the reported file with its full-file reference. A copied quote or brace can prevent dependency resolution.

Use Help me diagnose the module error.

Before you run the static check, predict whether the supplied Go starter contains a problem.

  • Vet every Go package by running:
go vet ./...

What should you see?

You should see no output. Exit status 0 confirms that the supplied Go starter passes the static check.

Did Go vet report a problem?

Use the reported file and line number to compare your file with the matching full-file reference. Check copied quotes and braces first.

Use Help me inspect the Go vet failure.

  • Confirm the module tool version by running:
go tool opa version

What should the version confirm?

The output should report version 1.21.4. This proves the repository invokes the pinned policy runtime.

  • Stage the complete starter kit by running:
git add .

What does this command stage?

The command stages the module files and every supplied starter artifact. The five design documents already belong to the first commit.

  • Check the Source Control panel for the staged starter files.
  • Create the second signed-off commit by running:
git commit -s -m "Add the repository kit"

What does the second commit preserve?

The commit isolates the starter implementation from the earlier design record. Its sign-off certifies the complete starter-kit contribution.

The local kit now has a clean two-commit history. The design leads the implementation exactly as planned.

Create the private collaboration space

The kit becomes reviewable when its repository, issue, branch, and automation share one private collaboration space. This preserves the privacy boundary while creating a complete contribution path.

The repository holds the Git history. GitHub Projects tracks issue 1 beside the work.

  • Create the private GitHub repository from the current folder by running:
gh repo create oss-repo-kit --private --source=. --push

What does this command publish?

The command creates private repository oss-repo-kit from the current folder. It pushes main through the signed-off starter-kit commit.

The first workflow run fails because the starter policy does not satisfy nine tests. That visible failure marks the implementation gap you close next.

  • Open the repository URL printed by the command in your browser.
  • Confirm that the first policy workflow run reports a failure.

Could not create the repository?

Confirm that GitHub CLI still uses the authenticated account with private repository access. An existing repository named oss-repo-kit can also block creation.

Use Help me diagnose the repository creation failure.

  • Return to the built-in terminal in oss-repo-kit.
  • Verify the repository visibility by running:
gh repo view --json visibility --jq .visibility

What should you see?

You should see PRIVATE. This confirms that the pushed repository kept the required visibility.

  • Create the personal Project by running:
gh project create --owner "@me" --title "oss-repo-kit"

What does this command create?

The command creates a Project named oss-repo-kit for your authenticated account. Its visibility requires a separate Project setting.

The visibility control is easy to miss because the creation command does not set it. Confirm the private state before you add issue 1.

  • Open the Project URL printed by the command in your browser.
  • Select Settings in the Project.
  • Set Visibility to Private.
  • Confirm that the Project settings show Private.
  • Return to the built-in terminal in oss-repo-kit.
  • Create issue 1 inside the private Project by running:
gh issue create --title "Build and release the OSS repository kit" --body "Implement, validate, document, merge, and release the private workflow policy kit." --project "oss-repo-kit"

What does the issue establish?

The issue names the complete contributor outcome. Adding it to the private Project gives the branch and future pull request a tracked unit of work.

Could not add the issue?

Confirm that the Project title is exactly oss-repo-kit. The authenticated account also needs the verified project scope.

Use Help me diagnose the Project issue failure.

  • Create the policy branch from the starter-kit commit by running:
git switch -c workflow-policy

What does this branch protect?

The workflow-policy branch starts from the signed-off starter-kit state. Your policy implementation can now become one reviewable change.

Before the final check, predict whether the design commit still appears before the starter-kit commit.

  • Verify the complete commit order by running:
git log --reverse --format=%s

What should the history show?

The first line should be Add the design docs. The second line should be Add the repository kit.

This order proves that the five design files were committed alone before implementation.

You now have a private contributor workspace with design evidence, a verified starter kit, issue 1, and a dedicated policy branch. Next, you will write the rules that turn nine failing tests into a complete policy suite.

Write and Prove the Workflow Policy

Your private repository now separates the design from the starter kit. The workflow-policy branch gives you a clean place to implement the rules.

The starter policy passes only the three cases that need no rejection. Nine failures expose the rules that are still missing.

You remain the policy author. Open Policy Agent runs your Rego rules against the supplied tests.

In this step, get ready to:
  • Observe the nine gaps in the starter policy.
  • Write deterministic checks for action pins, workflow permissions, and unsafe triggers.
  • Prove the policy with twelve tests plus two safe workflows.
Expose the starter policy's gaps

The supplied tests query data.workflow.deny as a set of finding messages. The starter rule never adds a message to that set.

Before you run the suite, how many tests do you think can pass when unsafe inputs produce no findings?

  • Reveal the starter policy's current behavior by running:
go tool opa test policy

What does this result prove?

You should see PASS: 3/12 plus FAIL: 9/12. The command exits with status 1 because required rejection cases failed.

The passing cases need no finding from the starter rule. The nine failures define the behavior your policy must add.

Seeing a different test summary?

Confirm that your terminal path ends in oss-repo-kit. Confirm that policy/workflow.rego still contains the supplied never-firing rule.

Check that policy/workflow_test.rego still contains all twelve supplied tests.

Help me diagnose a different starter-policy test result.

Workflow YAML is normalized into plain JSON before evaluation. The dump reveals the exact object shape that your policy receives.

  • Inspect the normalized safe workflow by running:
go run ./cmd/wfcheck -dump fixtures/good/release.yml

What does the dump show?

You should see an object containing on, permissions, and jobs. Its action reference ends with a full commit SHA.

Your policy evaluates this normalized structure. The supplied test objects use the same keys.

Workflow dump failing?

Confirm that fixtures/good/release.yml exists in the editor sidebar. Confirm that the terminal remains inside oss-repo-kit.

Help me diagnose the workflow dump command.

Write the three policy checks

The first rule protects external action references from movable tags and short hashes. A valid external reference has exactly 40 hexadecimal characters after @.

  • Select policy/workflow.rego from the editor sidebar.
  • Remove the supplied never-firing rule.
  • Keep package workflow at the top of the file.
  • Write learner-authored deny logic that examines each step-level uses value.
  • Allow local action paths that begin with ./.
  • Reject every invalid external reference with a finding beginning unpinned:.
  • Save policy/workflow.rego.

Before you rerun the suite, which pinning cases should change after this rule starts emitting findings?

  • Check the action-reference rule by running:
go tool opa test policy

What should change?

The suite should still exit with status 1 because two rule groups remain unfinished. The mutable-tag test plus the short-SHA test should now pass.

The local-action case must remain safe. A finding for that case means your rule is checking local paths as external actions.

Action-reference cases still failing?

Check every job for steps that contain uses. Exclude values that begin with ./ before validating the external reference.

Validate the complete text after @ as exactly 40 hexadecimal characters. A seven-character hash must still fail.

Help me debug my unpinned-action Rego rule.

The second rule applies least privilege to the workflow token. Top-level permissions must exist with read-only access.

  • Add learner-authored logic that detects a missing top-level permissions key.
  • Reject the literal value write-all.
  • Reject a permission map when any value is write.
  • Allow the literal value read-all.
  • Allow permission maps whose values remain read-only.
  • Start each rejected permission finding with permissions:.
  • Save policy/workflow.rego.

Before you test this change, which permission inputs should now receive findings?

  • Check the permission rules by running:
go tool opa test policy

What should the suite prove now?

The missing-permissions test should pass. The write-all test plus the mapped-write test should also pass.

The suite should still exit with status 1 because the four trigger shapes remain unenforced.

Permission cases still failing?

Handle a missing key separately from present values. Check the string form separately from the map form.

Reject a map when at least one permission has the value write. Keep read-only maps free from findings.

Help me debug my workflow-permissions Rego logic.

The final rule blocks pull_request_target in every supplied representation. The normalized on value can be a string, list, or object.

  • Add learner-authored logic that rejects pull_request_target when on is that string.
  • Reject pull_request_target when it appears in an on list.
  • Reject pull_request_target when it is an object key with typed configuration.
  • Reject pull_request_target when it is an object key with a null value.
  • Start every rejected trigger finding with trigger:.
  • Save policy/workflow.rego.

Before the complete policy test, do you expect all four trigger representations to produce the same finding prefix?

  • Prove the complete policy behavior by running:
go tool opa test policy

What does the passing suite prove?

You should now see PASS: 12/12. Every required unsafe input now produces a deterministic finding.

The safe cases still produce no findings. Your tests remain the decision authority.

Trigger cases still failing?

Compare your logic with the four trigger tests in policy/workflow_test.rego. Match each supplied input shape without changing the tests.

Check object-key membership independently of the key's value. This covers typed objects plus null object keys.

Help me debug my pull-request-target Rego checks.

Review, commit, and verify the policy

Passing tests proves the required rule behavior. Your design decisions also preserve the boundary between deterministic policy results and model advice.

  • Select docs/decisions.md from the editor sidebar.
  • Review both supplied decisions line by line.
  • Keep both decisions under ## My decisions.
  • Stage only the policy plus the reviewed decision document by running:
git add policy/workflow.rego docs/decisions.md

What does this command do?

This stages policy/workflow.rego plus docs/decisions.md for the next commit. No planted workflow fixtures belong in this commit.

Policy file not staged?

Confirm that you saved policy/workflow.rego before staging it. A saved completed policy should appear as a changed file in your editor.

Help me diagnose why my policy file was not staged.

A DCO sign-off is a contributor certification stored in the commit message. The -s option adds the trailer from your configured identity.

  • Create the signed-off policy commit by running:
git commit -s -m "Implement the workflow policy"

What should Git report?

Git should report a new commit with subject Implement the workflow policy. The commit records your authored policy on workflow-policy.

Commit not created?

If Git reports no staged changes, return to policy/workflow.rego and save the completed policy. Stage the two named paths again.

If Git cannot create the sign-off, confirm that the contributor name plus email checked during setup still exist.

Help me diagnose why my signed-off policy commit was not created.

Before the final test run, do you expect the committed policy to preserve all twelve passing cases?

  • Confirm the committed policy still passes its complete suite by running:
go tool opa test policy

What does the committed test prove?

You should see PASS: 12/12 again. The committed file still satisfies the complete test contract.

Tests failed after the commit?

Compare the saved policy/workflow.rego file with the version you tested before staging. Look for an unsaved editor buffer or an accidental edit.

Help me diagnose a post-commit policy test failure.

The unit suite proves isolated inputs. The wfcheck command proves your policy can evaluate real GitHub Actions workflow files.

Before you run the final check, how many workflow files do you expect to pass without findings?

  • Verify both supplied safe workflows by running:
go run ./cmd/wfcheck .github/workflows fixtures/good

What should you see?

You should see exactly two lines. The first is PASS .github/workflows/ci.yml.

The second line is PASS fixtures/good/release.yml. Both real workflows satisfy your policy.

Safe workflow check failing?

Use the finding prefix to identify the rule group that rejected a safe file. Compare that group with its matching allow case in policy/workflow_test.rego.

Keep the required rejection behavior intact while correcting the safe-case logic. Rerun both final commands after saving the fix.

Help me diagnose an unexpected safe-workflow finding.

Strong result. Your policy now proves twelve test cases plus two real safe workflows without giving model output any decision authority.

Your learner-authored policy is tested and committed with a sign-off. Next, you will plant unsafe workflows plus an unsigned commit to prove the controls fail closed.

Validate Findings and Sign-Offs

Your Open Policy Agent policy now passes all twelve tests. That proves the rules match the cases you designed.

A policy earns trust through observed pass behavior and observed fail behavior. In this step, you will plant three GitHub Actions defects to prove both sides.

You will also test commit history for sign-offs. The policy and recorded evidence decide every result.

In this step, get ready to:
  • Plant three bad workflow fixtures and capture their deterministic findings.
  • Detect an unsigned commit before removing it from history.
  • Measure the local model output and commit the completed validation record.
Plant failing workflows

A workflow fixture is a small input file built to exercise one policy rule. Each bad fixture isolates a different defect so the resulting finding has a clear cause.

  • In the Cursor Explorer sidebar, expand the fixtures folder.
  • Create a bad folder inside fixtures.
  • Create fixtures/bad/tag-pinned.yml by pasting this workflow:
name: tag-pinned
on:
  pull_request:
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

What does this fixture test?

  • The workflow declares read-only top-level permissions.
  • Its external action reference uses the mutable v7 tag.
  • Your policy should isolate that reference with an unpinned: finding.
  • Save fixtures/bad/tag-pinned.yml.
  • Confirm the Explorer sidebar lists tag-pinned.yml inside fixtures/bad.

Does the tag fixture look different?

Check the indentation beneath jobs and steps. Make sure the action reference ends with @v7.

Use help me compare the tag-pinned fixture if the file still differs.

  • Create fixtures/bad/write-default.yml by pasting this workflow:
name: write-default
on:
  pull_request:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

What does this fixture test?

  • The action reference uses a full commit SHA.
  • The workflow omits top-level permissions.
  • Your policy should isolate that omission with a permissions: finding.
  • Save fixtures/bad/write-default.yml.
  • Confirm the Explorer sidebar lists write-default.yml inside fixtures/bad.

Does the permissions fixture look different?

Confirm there is no top-level permissions key. Check that the action reference contains all 40 hexadecimal characters.

Use help me compare the write-default fixture if the file still differs.

  • Create fixtures/bad/pr-target.yml by pasting this workflow:
name: pr-target
on: pull_request_target
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

What does this fixture test?

  • The workflow has read-only top-level permissions.
  • Its action reference uses a full commit SHA.
  • The trigger uses pull_request_target as a string.
  • Your policy should isolate that trigger with a trigger: finding.
  • Save fixtures/bad/pr-target.yml.
  • Confirm the Explorer sidebar lists all three files inside fixtures/bad.

Does the trigger fixture look different?

Confirm pull_request_target appears on the same line as on:. Check the indentation beneath jobs.

Use help me compare the pr-target fixture if the file still differs.

✔️ Awesome, I've got everything!

All three bad workflow fixtures are saved. You are ready to test the policy against them.

ⓧ I'd like to double check the full code

name: tag-pinned
on:
  pull_request:
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

Tag Fixture Check

This complete file contains one mutable external action reference. Its permissions remain read-only.

name: write-default
on:
  pull_request:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

Permissions Fixture Check

This complete file contains a full action commit SHA. It has no top-level permissions declaration.

name: pr-target
on: pull_request_target
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

Trigger Fixture Check

This complete file uses the blocked trigger string. Its other tested controls remain safe.

  • Reconfirm the policy suite by running this command:
go tool opa test policy

What does this prove?

  • The command runs the twelve supplied policy tests.
  • You should still see PASS: 12/12.
  • This confirms the new fixture files did not alter your completed policy.

Are the policy tests failing now?

The command tests files inside policy. Confirm you did not edit policy/workflow.rego or policy/workflow_test.rego while creating the fixtures.

Use help me restore the passing policy suite if the suite still fails.

  • Recheck the two safe workflows by running this command:
go run ./cmd/wfcheck .github/workflows fixtures/good

What does this prove?

  • The command checks the policy workflow and the good release fixture.
  • You should see exactly two lines beginning with PASS.
  • These results prove the safe inputs still pass.

Do you see a safe workflow failure?

Read the finding prefix first. Compare the named file with the corresponding rule in policy/workflow.rego.

Use help me diagnose the unexpected safe-workflow finding if either safe file fails.

Before you run the planted fixtures, which three finding prefixes do you expect to see?

  • Evaluate all three bad workflows by running this command:
go run ./cmd/wfcheck -json fixtures/bad

What does this command produce?

  • The command evaluates every workflow inside fixtures/bad.
  • The -json option writes the findings to findings.json.
  • The file remains uncommitted because .gitignore already excludes it.

You will see three lines beginning with FAIL. The command exits with status 1 because findings exist.

The three failures should cover unpinned:, permissions:, and trigger:. That red result is the proof that your policy rejects the planted defects.

Did you get a different number of failures?

Match each failure to its fixture before changing anything. A missing prefix usually points to a fixture typo or a gap in policy/workflow.rego.

Use help me trace the planted workflow failures to trace the mismatch.

Expose the unsigned commit

A DCO sign-off is a contributor certification stored as a commit-message trailer. Your checker examines a Git revision range for that trailer.

You will plant one unsigned commit so the checker has a real history defect to inspect. The commit subject stays exactly Planted: no sign-off.

  • Stage only the three planted workflow fixtures by running this command:
git add fixtures/bad

What does this command stage?

  • The command stages the three files inside fixtures/bad.
  • The generated findings.json file remains ignored.
  • Create the planted unsigned commit by running this command:
git commit -m "Planted: no sign-off"

What did this commit plant?

  • The commit stores the three bad fixtures in history.
  • The command omits the sign-off option.
  • The missing trailer creates the history defect that signoff can detect.

Before you run the checker, do you think every commit in main..HEAD will pass?

  • Check every topic-branch commit for a sign-off by running this command:
go run ./cmd/signoff main..HEAD

What does the range check?

  • The revision range includes commits reachable from HEAD that are not reachable from main.
  • The checker labels signed-off commits with OK.
  • It labels a commit without the required trailer as MISSING.

You will see MISSING beside Planted: no sign-off. The command exits with status 1.

That failure is intentional. Your checker has now proved it can reject an unsigned contribution.

Removing a commit can feel risky. Here it only discards the unpublished planted tip.

The mixed reset keeps the three fixture files in your working tree. Your signed policy commit remains in history.

  • Remove the unsigned tip commit by running this command:
git reset HEAD~1

What does this reset preserve?

  • The command removes the tip commit from branch history.
  • It resets the staging area.
  • It leaves the three bad workflow files in the working tree.

Before you rerun the checker, do you expect any commit to show MISSING now?

  • Recheck the topic-branch history by running this command:
go run ./cmd/signoff main..HEAD

What should the history show now?

  • Every remaining topic-branch commit should print an OK line.
  • No line should begin with MISSING.
  • This proves the unsigned commit is no longer in the checked range.

Do you still see MISSING?

Read the subject on each MISSING line. The planted subject should no longer appear after the reset.

Use help me inspect the remaining unsigned commit if another subject is reported.

Measure and record the evidence

The fix-list command sends the generated findings only to your local Ollama model. Deterministic matching code scores the returned items against the real findings.

The local response can take close to two minutes because the command allows that much time for the model call. A quiet terminal during generation does not mean the process has stalled.

Before you run the scorer, do you think every proposed item will match one of the three findings?

  • Generate the local fix list by running this command:
go run ./cmd/fixlist

How is the model output measured?

  • The model drafts plain-language fixes from findings.json.
  • The command matches each proposed file and rule against a real finding.
  • It strikes an item with NO MATCH when deterministic checks reject it.
  • It writes the measured list to the ignored fixlist.md file.

You will see a line beginning with Measured:. Its score reports coverage against the three real findings.

Record the exact struck NO MATCH item when one exists. Record No struck item observed when every returned item matches.

Did the local model call fail?

Confirm findings.json still exists in the Explorer sidebar. Confirm your local model service is available.

Use help me diagnose the local fix-list command if the command still fails.

The test plan now becomes the permanent evidence record. Copy observations exactly because estimates cannot prove policy behavior.

  • Select docs/test-plan.md in the Explorer sidebar.
  • Fill the first two Observed result cells with the policy summary and both safe-workflow lines.
  • Save docs/test-plan.md.

The first two rows should now contain observed terminal output instead of the original paste prompts.

  • Fill the next two Observed result cells with all three planted failure lines plus the unsigned-commit result.
  • Include exit status 1 in both cells.
  • Save docs/test-plan.md.

The planted workflow row should contain three FAIL lines. The planted commit row should contain the MISSING line.

  • Fill the sign-off-after-reset cell with every observed OK line.
  • Fill the fix-list cell with the exact Measured: line.
  • Add the exact struck NO MATCH item when one exists.
  • Enter No struck item observed when no item was struck.
  • Save docs/test-plan.md.

Every Observed result cell should now contain evidence from this validation run. No original paste prompt should remain in that column.

  • Stage the planted fixtures and completed validation record by running this command:
git add fixtures/bad docs/test-plan.md

What enters the validation commit?

  • The command stages the three bad workflow fixtures.
  • It also stages the completed docs/test-plan.md record.
  • The ignored findings.json and fixlist.md files remain local evidence.
  • Create the signed validation commit by running this command:
git commit -s -m "Add workflow policy validation"

What does this commit preserve?

  • The commit records the three policy failures as reusable fixtures.
  • It records your observed test results.
  • The sign-off option adds the contributor certification trailer from your configured identity.

Before the final check, do you expect the policy suite and the topic-branch sign-offs to pass together?

  • Verify the completed policy and commit history by running these commands:
go tool opa test policy
go run ./cmd/signoff main..HEAD

What should the final check prove?

  • The policy command should report PASS: 12/12.
  • The sign-off command should print only OK lines.
  • The validation commit should appear among those signed-off commits.

Strong result. Your repository now contains reproducible red cases plus an observed validation record.

The model produced advice, but deterministic code measured it. Your policy result never depended on the model's wording.

Did the final verification fail?

Use the first failing command to narrow the cause. A policy failure points to policy/workflow.rego, while a MISSING line names the unsigned commit.

Use help me diagnose the final validation check to inspect the result.

Your evidence is tested, measured, and committed. Next up, you will turn this branch into one reviewable pull request and a private release.

Merge and Release the Kit

Your workflow policy now passes all twelve tests. Your validation record also captures every planted failure.

This step records the handoff in Git history. GitHub Actions then proves the change before the merge.

In this step, get ready to:
  • Commit the final handoff documents.
  • Merge the CI-approved pull request.
  • Publish the merged kit as private release v0.1.0.
Write the handoff documents

The final documents explain what the kit proves. They also preserve its limits before review begins.

  • In your editor, select README.md from the oss-repo-kit folder.
  • Replace the existing skeleton with this opening content:
# The Open-Source Repo Kit: Pinned Actions, Read-Only Tokens and Signed Commits, Proven by Your Own Policy

System used: Windows with PowerShell in the editor's built-in terminal.

## Overview

I built this as a contributor. I designed first. I wrote the policy. I planted three bad workflows and one unsigned commit. I proved they failed. I removed the unsigned commit. I measured a local fix list. I merged one pull request and made one private release. I left the upstream move as an unposted draft.

## Problem

On 2026-03-19, a threat actor force-pushed 76 of 77 version tags in `aquasecurity/trivy-action` to malicious commits. Source: Aqua Security advisory GHSA-69fq-xp46-6x23, published 2026-03-21.

A full commit SHA prevents a moved tag from changing the selected action code. It does not prove that code is safe. It does not fix actions that other projects still name by tag.

What does this section record?

  • The overview records the complete contributor workflow in first person.
  • The problem section connects the policy to the documented action-tag incident.
  • The final paragraph states the security boundary of a full commit SHA.
  • Save README.md.
  • Confirm the file now shows the system record followed by the Overview section.

Does the README still show the skeleton?

  • Confirm you edited README.md inside oss-repo-kit.
  • Compare the first heading with the code block above.
  • Help me compare my README opening with the required version.
  • Place your cursor after the final sentence in the Problem section.
  • Add the project checks by pasting this content below it:
## Checks

```text
go vet ./...
```

```text
go tool opa test policy
```

```text
go run ./cmd/wfcheck .github/workflows fixtures/good
```

```text
go run ./cmd/wfcheck -json fixtures/bad
```

```text
go run ./cmd/signoff main..HEAD
```

```text
go run ./cmd/fixlist
```

What do these checks cover?

  • The commands cover Go analysis plus policy tests.
  • They also cover safe workflows plus planted failures.
  • The final checks cover commit sign-offs plus measured local-model advice.
  • Save README.md.
  • Confirm the Checks section shows six separate commands.

Are any checks missing?

  • Count the six fenced command blocks under Checks.
  • Restore any missing command from the code block above.
  • Help me find a missing README check.
  • Place your cursor after the final check in README.md.
  • Complete the README by pasting these final sections:
## Authority

My policy and its tests decide. `qwen3:1.7b` only drafts fixes. Deterministic code labels the result `Measured`.

## CNCF levels

Open Policy Agent is a CNCF Graduated project. Scorecard, Go, Ollama, GitHub Actions, and Dependabot are not CNCF projects.

## AI disclosure

AI help was used. I reviewed every line. I own the result.

## Privacy

The repository and Project remain private. `docs/upstream.md` is a draft. This project did not post it and does not claim it was merged.

Why do these sections matter?

  • The authority section keeps the policy plus its tests in control.
  • The disclosure assigns responsibility for every AI-assisted line.
  • The privacy section records the boundary around the upstream draft.
  • Save README.md.
  • Confirm the final line states that the upstream draft was not posted.

Does the README end early?

  • Check that Authority appears immediately after the final check.
  • Check that Privacy is the last section.
  • Help me check the final README sections.

✔️ Awesome, I've got everything!

Your final README now records the evidence, authority, disclosure, limits, plus privacy boundary.

ⓧ I'd like to double check the full code

# The Open-Source Repo Kit: Pinned Actions, Read-Only Tokens and Signed Commits, Proven by Your Own Policy

System used: Windows with PowerShell in the editor's built-in terminal.

## Overview

I built this as a contributor. I designed first. I wrote the policy. I planted three bad workflows and one unsigned commit. I proved they failed. I removed the unsigned commit. I measured a local fix list. I merged one pull request and made one private release. I left the upstream move as an unposted draft.

## Problem

On 2026-03-19, a threat actor force-pushed 76 of 77 version tags in `aquasecurity/trivy-action` to malicious commits. Source: Aqua Security advisory GHSA-69fq-xp46-6x23, published 2026-03-21.

A full commit SHA prevents a moved tag from changing the selected action code. It does not prove that code is safe. It does not fix actions that other projects still name by tag.

## Checks

```text
go vet ./...
```

```text
go tool opa test policy
```

```text
go run ./cmd/wfcheck .github/workflows fixtures/good
```

```text
go run ./cmd/wfcheck -json fixtures/bad
```

```text
go run ./cmd/signoff main..HEAD
```

```text
go run ./cmd/fixlist
```

## Authority

My policy and its tests decide. `qwen3:1.7b` only drafts fixes. Deterministic code labels the result `Measured`.

## CNCF levels

Open Policy Agent is a CNCF Graduated project. Scorecard, Go, Ollama, GitHub Actions, and Dependabot are not CNCF projects.

## AI disclosure

AI help was used. I reviewed every line. I own the result.

## Privacy

The repository and Project remain private. `docs/upstream.md` is a draft. This project did not post it and does not claim it was merged.

How should I use this reference?

Compare this reference with your saved README.md. Every line should match exactly.

The README explains the result. The next three documents preserve the operating limits plus the unposted upstream path.

  • Use your editor's file list to create limits.md inside the existing docs folder.
  • Add the policy boundaries by pasting this content:
# Limits

- A full commit SHA prevents a moved tag from selecting different code. It does not prove the selected code is safe.
- This does not fix actions that other projects still name by tag.
- The local policy is not OpenSSF Scorecard.
- Action pinning covers step-level external `uses` values and allows local paths beginning `./`.
- Permission checks cover top-level workflow permissions, not job-level rules.
- Trigger checks cover the four tested `pull_request_target` shapes. They do not inspect `workflow_run`, script injection, context logging, or secret use.
- A DCO sign-off is a commit-message certification, not a cryptographic signature.
- Model output can be wrong. The model decides nothing.
- GitHub Actions must stay within included compute to keep cost at 0.00 USD.

What do the limits protect?

This file prevents the kit from claiming broader security coverage than it proves. Each boundary points to work outside this project's tested scope.

  • Save docs/limits.md.
  • Confirm the file ends with the 0.00 USD compute boundary.

Is a limit missing?

  • Compare the nine bullets with the code block above.
  • Check the backticks around every named workflow value.
  • Help me check the policy limits document.

✔️ Awesome, I've got everything!

Your limits document now states every tested boundary.

ⓧ I'd like to double check the full code

# Limits

- A full commit SHA prevents a moved tag from selecting different code. It does not prove the selected code is safe.
- This does not fix actions that other projects still name by tag.
- The local policy is not OpenSSF Scorecard.
- Action pinning covers step-level external `uses` values and allows local paths beginning `./`.
- Permission checks cover top-level workflow permissions, not job-level rules.
- Trigger checks cover the four tested `pull_request_target` shapes. They do not inspect `workflow_run`, script injection, context logging, or secret use.
- A DCO sign-off is a commit-message certification, not a cryptographic signature.
- Model output can be wrong. The model decides nothing.
- GitHub Actions must stay within included compute to keep cost at 0.00 USD.

How should I use this reference?

Compare this reference with your saved docs/limits.md. The nine boundaries should match exactly.

  • Use your editor's file list to create upstream.md inside the existing docs folder.
  • Add the unposted documentation issue draft by pasting this content:
# Draft Documentation Issue: State a Free Private-Repository Fallback

## Problem

A contributor may need a 0.00 USD, private repository. The Scorecard Action path for private repositories depends on paid GitHub security capability. A narrow local policy can teach selected controls, but it must not claim to be Scorecard.

## Example

This repository tests three ideas:

1. External workflow actions use full commit SHAs.
2. Top-level workflow permissions are explicit and read-only.
3. Tested `pull_request_target` forms are rejected.

The example includes twelve policy tests, four workflow fixtures, a DCO range check, and deterministic measurement of local-model fixes.

## Limits

It does not inspect every dependency, job-level permission, `workflow_run`, script injection, context logging, secret use, or actions in another repository. A full SHA does not prove selected code is safe.

## Contribution note

I wrote and reviewed this draft with disclosed AI help. This private project did not post it. It is not merged upstream.

Recommendation: Add a clearly labelled local, partial fallback for private contributors while directing readers to full Scorecard when their constraints allow it.

Why keep this as a draft?

This file proposes a bounded documentation change. Its contribution note prevents the private draft from being mistaken for published upstream work.

  • Save docs/upstream.md.
  • Confirm the final line begins with Recommendation:.

Does the draft look incomplete?

  • Confirm the file contains the Problem plus Example sections.
  • Confirm the contribution note says the project did not post the draft.
  • Help me check the upstream draft.

✔️ Awesome, I've got everything!

Your upstream proposal is now preserved as a reviewed private draft.

ⓧ I'd like to double check the full code

# Draft Documentation Issue: State a Free Private-Repository Fallback

## Problem

A contributor may need a 0.00 USD, private repository. The Scorecard Action path for private repositories depends on paid GitHub security capability. A narrow local policy can teach selected controls, but it must not claim to be Scorecard.

## Example

This repository tests three ideas:

1. External workflow actions use full commit SHAs.
2. Top-level workflow permissions are explicit and read-only.
3. Tested `pull_request_target` forms are rejected.

The example includes twelve policy tests, four workflow fixtures, a DCO range check, and deterministic measurement of local-model fixes.

## Limits

It does not inspect every dependency, job-level permission, `workflow_run`, script injection, context logging, secret use, or actions in another repository. A full SHA does not prove selected code is safe.

## Contribution note

I wrote and reviewed this draft with disclosed AI help. This private project did not post it. It is not merged upstream.

Recommendation: Add a clearly labelled local, partial fallback for private contributors while directing readers to full Scorecard when their constraints allow it.

How should I use this reference?

Compare this reference with your saved docs/upstream.md. Keep the recommendation as the final line.

  • Use your editor's file list to create after-action-review.md inside the existing docs folder.
  • Add the result plus the minute-60 stop record by pasting this content:
# After-Action Review

## Result

One private repository. One private Project. One issue. One pull request. One private release. Cost: 0.00 USD.

## Evidence

The pasted results are in `docs/test-plan.md`. The policy and tests decided. The model output was labelled `Measured`.

## What worked

Design came first. The starter failed before the contributor policy existed. Planted workflow and sign-off failures were visible and removed or corrected before merge.

## What remains

Other projects can still name actions by mutable tags. A full SHA can select unsafe code. The upstream draft remains unposted.

## Timebox

Stopped at minute: 60

Step at stop: Close-Out verification

What does this review preserve?

The review connects the shipped result to its evidence. It also records unfinished work plus the fixed stopping point.

  • Save docs/after-action-review.md.
  • Confirm the final two records show minute 60 plus Close-Out verification.

Is the stop record different?

  • Restore Stopped at minute: 60 exactly.
  • Restore Step at stop: Close-Out verification exactly.
  • Help me check the after-action review.

✔️ Awesome, I've got everything!

Your after-action review now records the result plus the fixed timebox.

ⓧ I'd like to double check the full code

# After-Action Review

## Result

One private repository. One private Project. One issue. One pull request. One private release. Cost: 0.00 USD.

## Evidence

The pasted results are in `docs/test-plan.md`. The policy and tests decided. The model output was labelled `Measured`.

## What worked

Design came first. The starter failed before the contributor policy existed. Planted workflow and sign-off failures were visible and removed or corrected before merge.

## What remains

Other projects can still name actions by mutable tags. A full SHA can select unsafe code. The upstream draft remains unposted.

## Timebox

Stopped at minute: 60

Step at stop: Close-Out verification

How should I use this reference?

Compare this reference with your saved docs/after-action-review.md. The timebox lines should close the file.

  • Stage the four handoff documents by running this command:
git add README.md docs/limits.md docs/upstream.md docs/after-action-review.md

What does this command stage?

This command stages only the final README plus the three new handoff documents. The ignored evidence files remain outside the commit.

The terminal returns to the prompt without listing the ignored evidence files.

Seeing a missing path?

  • Check that each new document sits directly inside docs.
  • Check that README.md remains directly inside oss-repo-kit.
  • Help me fix the handoff staging paths.
  • Create the signed-off handoff commit by running this command:
git commit -s -m "Document the policy handoff"

What does this commit prove?

The signed-off commit binds the final documentation to your contributor identity. Its exact subject makes the handoff easy to locate during review.

You should see a new commit with subject Document the policy handoff.

Did the commit fail?

  • Check that every handoff file was saved before staging.
  • Check that your configured Git identity is still available.
  • Help me diagnose the handoff commit failure.
Open and merge the pull request

The handoff commit is ready for review. The pull request now gives CI one final chance to block an unsafe merge.

  • Push the workflow-policy branch to the private repository by running this command:
git push -u origin workflow-policy

What does this push establish?

The push creates the remote topic branch. It also sets that branch as the local upstream.

You should see confirmation that workflow-policy now exists on the private remote.

Did the push fail?

  • Confirm the terminal still points to ~/oss-repo-kit.
  • Confirm your authenticated GitHub account still has access to the private repository.
  • Help me diagnose the branch push failure.
  • Create the pull request with the required AI disclosure by running this command:
gh pr create --base main --head workflow-policy --title "Add the workflow policy kit" --body "Closes #1. AI help was used. I reviewed every line."

What does the pull request record?

  • The base plus head values define the exact review direction.
  • The body discloses AI assistance.
  • The closing keyword links the merge to issue 1.

You should see one pull request URL for Add the workflow policy kit.

Was the pull request rejected?

  • Confirm the remote branch finished pushing before creating the pull request.
  • Confirm no pull request already exists for workflow-policy.
  • Help me diagnose the pull request creation failure.

Before you watch the checks, predict whether the policy job should pass with the validated branch.

The first report can take a few minutes while GitHub Actions starts the runner. A short wait here is expected.

  • Watch the pull request checks until completion by running this command:
gh pr checks --watch

What does watch mode prove?

Watch mode keeps the terminal attached to the pull request checks. It finishes after CI reports its result.

You should see the policy check pass. That result proves the reviewed branch satisfied CI before merge.

Are no checks reported?

  • Wait for GitHub Actions to register the workflow.
  • Repeat the watch command above.
  • Help me diagnose the missing or failed pull request check.

The next command deletes only the topic branch after the merge. Your reviewed work remains on main.

  • Merge the approved pull request by running this command:
gh pr merge --merge --delete-branch

What does this merge complete?

The command creates a merge commit on main. It removes the reviewed remote topic branch afterward.

The pull request now shows a merged state. Issue 1 closes through the pull request body.

Is the pull request blocked?

  • Confirm the policy check reached a passing state.
  • Confirm the pull request still targets main.
  • Help me diagnose the blocked merge.
  • Return to the pull request URL from the creation output.
  • Confirm the pull request shows a merged state.
  • Return to issue 1 in the private repository.
  • Confirm issue 1 shows a closed state.
Create the release and verify privacy

The merged commit is now the release source. The final checks prove that shipping did not weaken the project's privacy boundary.

  • Create private release v0.1.0 by running this command:
gh release create v0.1.0 --title "v0.1.0" --notes "Initial private release of oss-repo-kit."

What does this release publish?

The command creates release v0.1.0 with the required title plus notes. The release remains inside the private repository.

You should see a URL for release v0.1.0.

Did release creation fail?

  • Confirm the pull request merged before creating the release.
  • Confirm release tag v0.1.0 does not already exist.
  • Help me diagnose the release creation failure.

Before the final repository check, predict which visibility plus release values should appear.

  • Verify the repository visibility plus latest release by running this command:
gh repo view --json visibility,latestRelease --jq '{visibility: .visibility, release: .latestRelease.tagName}'

What does this query verify?

The query reads the repository visibility plus latest release tag. It checks both release completion and the privacy boundary in one result.

You should see visibility PRIVATE plus release v0.1.0.

Is a final value wrong?

  • Confirm the repository value remains PRIVATE.
  • Confirm the latest release value is v0.1.0.
  • Help me diagnose the final repository query.
  • Return to the existing oss-repo-kit Project.
  • Select Settings.
  • Confirm Visibility shows Private.
  • Return to docs/upstream.md in your editor.
  • Confirm the draft still ends with its Recommendation: line.
  • Keep the upstream draft unposted.
  • Stop all project work at minute 60.
  • Confirm docs/after-action-review.md records Stopped at minute: 60.
  • Confirm the same file records Step at stop: Close-Out verification.

That is the complete handoff. Your private kit now has a CI-approved merge, a closed issue, a private release, plus an upstream draft that stayed under your control.

Secret mission

Audit the Released Evidence

Reproduce the privacy check, policy evidence, planted failures, and measured local-model result from your finished release. Then prove the audit left every tracked file unchanged.

Clean Up Your Resources

Clean Up Your Resources

Your kit costs 0.00 USD on its current path. Choose the cleanup option that matches what you want to do next.

Resources you used:

  • Your local Git repository at ~/oss-repo-kit. It contains the source files plus the ignored findings.json and fixlist.md evidence files.
  • Your private GitHub repository named oss-repo-kit. It contains issue #1, the merged pull request, and release v0.1.0.
  • Your private GitHub Project named oss-repo-kit.

Keep everything running

No action is needed. Choose this option if you want to keep the completed kit as private engineering evidence.

  • Keep the oss-repo-kit repository private.
  • Keep the oss-repo-kit Project private.
  • Leave docs/upstream.md unposted.
  • Retain ~/oss-repo-kit with its local evidence files.

Pause - I'll come back to this later

Pause means leaving the completed evidence unchanged while you step away. No visibility change is needed.

  • Leave the repository visibility set to private.
  • Leave the Project visibility set to private.
  • Keep docs/upstream.md inside the repository as an unposted draft.
  • Return later by reopening ~/oss-repo-kit in your editor.

Delete - I don't want to use this again

Deletion is permanent, so preserve any evidence you want before you continue. Removing the GitHub repository also removes its issue, merged pull request, and release.

Delete the GitHub Repository
  • Open the private oss-repo-kit repository in GitHub.
  • Open the repository settings page.
  • Scroll to the repository deletion area.
  • Start the repository deletion process.
  • Enter oss-repo-kit when GitHub requests the repository name.
  • Complete the final deletion confirmation.
  • Reload the original repository page.

You should see that the repository is unavailable. This confirms that issue #1, the merged pull request, and release v0.1.0 are gone with it.

Delete the GitHub Project
  • Open the private oss-repo-kit Project from your GitHub Projects list.
  • Select Settings.
  • Scroll to the Project deletion area.
  • Start the Project deletion process.
  • Complete the final deletion confirmation.
  • Return to your GitHub Projects list.

You should no longer see oss-repo-kit in the list. The private Project has now been removed.

Delete the Local Repository

Your terminal still points to ~/oss-repo-kit. The first command moves to your home folder before the second removes the repository.

  • Delete the local repository and its ignored evidence files by running these PowerShell commands:
cd ~
Remove-Item -Recurse -Force ~/oss-repo-kit

What Do These Commands Do?

  • The first line leaves the repository directory so Windows can remove it.
  • The second line recursively deletes ~/oss-repo-kit. This includes findings.json and fixlist.md.
  • Press the Windows key to open search.
  • Type File Explorer.
  • Press Enter.
  • Check your home folder for oss-repo-kit.

You should no longer see the oss-repo-kit folder. Your local repository and both ignored evidence files are deleted.

Windows Still Has the Folder Open?

Close editor windows that still use ~/oss-repo-kit. Start a new PowerShell terminal from your home folder before you retry the deletion.

Ask for help with a locked local repository folder.

Remove the Local Model

The model may have existed before this project. Remove it only if Setup downloaded it for this kit and you no longer need it.

  • Open the local model management view in Ollama.
  • Remove qwen3:1.7b.
  • Confirm that qwen3:1.7b no longer appears in your installed model list.

Nice Work!

Nice Work!

You did it! Your private oss-repo-kit repository now carries a tested workflow policy from design through release v0.1.0.

You've learned how to:

  • Build a design-first contribution history with five design files committed before any code. Your own policy as code became the decision authority. Its twelve tests proved the required behavior.
  • Create observable validation across two safe workflows. You exposed three planted workflow failures. You caught the unsigned commit through a DCO sign-off check.
  • Measure local-model advice against actual findings through deterministic scoring. You shipped the reviewed kit through one private pull request after GitHub Actions passed. Release v0.1.0 marks the final handoff. The reviewed docs/upstream.md draft remains unposted.
  • Secret Mission: Extend this foundation through an optional challenge that pushes your repository policy skills further.

Ready to quiz yourself?