Skip to content

[formal-spec] security-architecture-spec-validation.md — Formal model & test suite — 2026-08-12 #52324

Description

@github-actions

Summary

Formalized specs/security-architecture-spec-validation.md, focusing this run on the §12 Compliance Test Matrix Gap Analysis for Permission Management (T-PM-001 to T-PM-007). Prior notes flagged T-PM-003 (strict mode), T-PM-005 (workflow_run repository validation), and T-PM-007 (token validation) as lacking dedicated evidence entries. This run derives formal predicates directly from the real exported implementation (buildWorkflowRunRepoSafetyCondition, validateWorkflowRunBranches, Compiler.strictMode/SetStrictMode, getEffectiveGitHubToken, getEffectiveSafeOutputGitHubToken) and produces a testify suite encoding acceptance/rejection boundaries for each.

Specification

  • File: specs/security-architecture-spec-validation.md
  • Focus area: Permission Management gap analysis (T-PM-003, T-PM-005, T-PM-007) plus supporting workflow_run trigger validation (agent_validation.go, role_checks.go, github_token.go)
  • Formal notation used: TLA+ (guard/condition composition) / Z3-style guard conjunction / F* (pre/post contracts)

Formal Model

Predicates and invariants (illustrative notation)
-- P1: WorkflowRunRepoSafetyCondition (T-PM-005)
-- Source: role_checks.go buildWorkflowRunRepoSafetyCondition
--   "This allows all non-workflow_run events, but requires repository match
--    and fork check for workflow_run events"
-- TLA+ style:
--   Cond == (event_name /= "workflow_run") \/ (repo.id = repository_id /\ ~repo.fork)
∀ event ∈ Events:
  SafeToRun(event) ⇔
    event.name ≠ "workflow_run"
    ∨ (event.workflow_run.repository.id = github.repository_id
       ∧ ¬event.workflow_run.repository.fork)

-- P2: WorkflowRunRepoSafetyOnlyAppliesWhenTriggerPresent
-- Source: compiler_jobs.go — "Determine if we need to add workflow_run repository safety check"
∀ frontmatter:
  hasWorkflowRunTrigger(frontmatter) = false
    ⇒ workflowRunRepoSafety = "" (no guard injected; non-workflow_run workflows unaffected)

-- P3: WorkflowRunRequiresNonEmptyWorkflowsField
-- Source: agent_validation.go validateWorkflowRunHasWorkflows
--   "GitHub Actions requires on.workflow_run.workflows to reference at least one workflow"
∀ wr ∈ WorkflowRunTriggers:
  Valid(wr) ⇒ NonEmpty(wr.workflows)

-- P4: WorkflowRunBranchRestrictionModeSensitive (T-PM-003, strict mode)
-- Source: agent_validation.go emitWorkflowRunMissingBranches
--   "if c.strictMode { return error } else { warn }"
∀ wr ∈ WorkflowRunTriggers, strict ∈ Bool:
  ¬HasBranches(wr) ⇒
    (strict = true  ⇒ Result = CompileError)
    ∧ (strict = false ⇒ Result = Warning ∧ WarningCount' = WarningCount + 1)

-- P5: WorkflowRunBranchRestrictionSatisfiedNoOp
-- Source: agent_validation.go validateWorkflowRunBranches
∀ wr ∈ WorkflowRunTriggers:
  HasBranches(wr) ⇒ Result = OK ∧ WarningCount' = WarningCount

-- P6: NoWorkflowRunTriggerIsNoOp
-- Source: agent_validation.go validateWorkflowRunBranches
--   "if !strings.Contains(workflowData.On, "workflow_run") { return nil }"
∀ on ∈ TriggerStrings:
  ¬Contains(on, "workflow_run") ⇒ validateWorkflowRunBranches(on) = OK (skip, no error, no warning)

-- P7: DefaultGitHubTokenPrecedence (T-PM-007)
-- Source: github_token.go getEffectiveGitHubToken
--   "1. Custom token passed as parameter ... 2. Default fallback:
--    secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN"
∀ customToken ∈ String:
  customToken ≠ "" ⇒ EffectiveToken(customToken) = customToken
  customToken = "" ⇒ EffectiveToken(customToken) =
    "${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}"

-- P8: SafeOutputGitHubTokenPrecedence (T-PM-007, write-job isolation)
-- Source: github_token.go getEffectiveSafeOutputGitHubToken
--   "This simpler chain ensures safe outputs use: safe outputs token -> GH_AW_GITHUB_TOKEN
--    -> GitHub Actions token"
∀ customToken ∈ String:
  customToken ≠ "" ⇒ EffectiveSafeOutputToken(customToken) = customToken
  customToken = "" ⇒ EffectiveSafeOutputToken(customToken) =
    "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}"

-- P9: TokenChainsAreDistinctByJobRole
-- Source: cross-reference of getEffectiveGitHubToken vs getEffectiveSafeOutputGitHubToken
--   default fallback chains must differ: agent/tool token includes MCP-server-specific
--   secret first, safe-output token chain omits it (write-scoped token isolation)
DefaultTokenChain(role="tool") ≠ DefaultTokenChain(role="safe_output")
  ∧ Contains(DefaultTokenChain("tool"), "GH_AW_GITHUB_MCP_SERVER_TOKEN")
  ∧ ¬Contains(DefaultTokenChain("safe_output"), "GH_AW_GITHUB_MCP_SERVER_TOKEN")

-- P10: StrictModeIsPerCompilerInstance (T-PM-003 gating primitive)
-- Source: compiler_types.go strictMode field, compiler_mutators.go SetStrictMode
∀ c ∈ Compiler:
  SetStrictMode(c, true).strictMode = true
  ∧ SetStrictMode(c, false).strictMode = false
  (idempotent setter; no default side effects beyond boolean assignment)

-- P11: BashRestrictionWildcardSafe (supporting evidence, carried from prior CTR notes,
-- cross-referenced here as another engine-capability compile-time gate akin to T-PM-003)
-- Source: agent_validation.go HasBashExplicitRestriction
∀ tools ∈ ToolsMap:
  tools = nil ⇒ HasBashExplicitRestriction(tools) = false
  ∧ (tools.bash = ["*"] ∨ tools.bash = [":*"]) ⇒ HasBashExplicitRestriction(tools) = false
  ∧ tools.bash = false ⇒ HasBashExplicitRestriction(tools) = true
  ∧ tools.bash = [] ⇒ HasBashExplicitRestriction(tools) = true

Behavioral Coverage Map

Predicate / Invariant Test Function Description
WorkflowRunRepoSafetyCondition (P1) TestFormalPM005_WorkflowRunRepoSafetyCondition Verifies the compiled if: guard checks repo-id equality and non-fork status only for workflow_run events
WorkflowRunRepoSafetyOnlyAppliesWhenTriggerPresent (P2) TestFormalPM005_WorkflowRunRepoSafetyOnlyWhenTriggerDeclared Confirms hasWorkflowRunTrigger correctly detects map/string/absent on: forms
WorkflowRunRequiresNonEmptyWorkflowsField (P3) TestFormalPM_WorkflowRunRequiresNonEmptyWorkflows Table-driven: string, []string, []any, empty, nil
WorkflowRunBranchRestrictionModeSensitive (P4) TestFormalPM003_StrictModeGatesMissingBranchRestriction Missing branches → error in strict mode, warning otherwise
WorkflowRunBranchRestrictionSatisfiedNoOp (P5) TestFormalPM_BranchRestrictionPresentIsNoOp Present branches never triggers strict/warn path
NoWorkflowRunTriggerIsNoOp (P6) TestFormalPM_NoWorkflowRunTriggerSkipsValidation Non-workflow_run triggers bypass validation entirely
DefaultGitHubTokenPrecedence (P7) TestFormalPM007_DefaultGitHubTokenPrecedence Custom token wins; else 3-tier secret fallback chain
SafeOutputGitHubTokenPrecedence (P8) TestFormalPM007_SafeOutputTokenPrecedence Custom token wins; else 2-tier safe-output fallback chain
TokenChainsAreDistinctByJobRole (P9) TestFormalPM007_TokenChainsDifferByRole Tool-token chain includes MCP-server secret; safe-output chain excludes it
StrictModeIsPerCompilerInstance (P10) TestFormalPM003_SetStrictModeIsIdempotentSetter SetStrictMode toggles strictMode field deterministically
BashRestrictionWildcardSafe (P11) TestFormalPM_BashExplicitRestrictionBoundary nil/wildcard/false/empty-list boundary matrix (edge cases)

Generated Test Suite

📄 `pkg/workflow/security_architecture_pm_formal_test.go`
// Package workflow_test encodes the formal predicates derived from
// specs/security-architecture-spec-validation.md, §12 Compliance Test Matrix
// Gap Analysis, Permission Management row (T-PM-001 to T-PM-007).
//
// This suite specifically closes the previously identified evidence gaps:
//   - T-PM-003 (strict mode gating of workflow_run branch-restriction warnings/errors)
//   - T-PM-005 (repository validation guard for workflow_run triggers)
//   - T-PM-007 (GitHub token precedence/validation for tool vs. safe-output jobs)
//
// Predicates encoded (see issue body "Formal Model" section for full notation):
//   P1  WorkflowRunRepoSafetyCondition
//   P2  WorkflowRunRepoSafetyOnlyAppliesWhenTriggerPresent
//   P3  WorkflowRunRequiresNonEmptyWorkflowsField
//   P4  WorkflowRunBranchRestrictionModeSensitive (T-PM-003)
//   P5  WorkflowRunBranchRestrictionSatisfiedNoOp
//   P6  NoWorkflowRunTriggerIsNoOp
//   P7  DefaultGitHubTokenPrecedence (T-PM-007)
//   P8  SafeOutputGitHubTokenPrecedence (T-PM-007)
//   P9  TokenChainsAreDistinctByJobRole (T-PM-007)
//   P10 StrictModeIsPerCompilerInstance (T-PM-003)
//   P11 BashRestrictionWildcardSafe (supporting engine-capability gate)
package workflow_test

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/github/gh-aw/pkg/workflow"
)

// ---------------------------------------------------------------------------
// P1 / T-PM-005: workflow_run repository safety condition (compiled `if:` guard)
// ---------------------------------------------------------------------------

func TestFormalPM005_WorkflowRunRepoSafetyCondition(t *testing.T) {
	t.Parallel()

	c := workflow.NewCompiler()

	frontmatter := map[string]any{
		"on": map[string]any{
			"workflow_run": map[string]any{
				"workflows": []any{"CI"},
			},
		},
	}

	hasTrigger := c.HasWorkflowRunTriggerForTest(frontmatter)
	require.True(t, hasTrigger, "workflow_run trigger must be detected from map-form 'on:' section (P2)")

	condition := c.BuildWorkflowRunRepoSafetyConditionForTest()

	assert.True(t, strings.Contains(condition, "workflow_run"),
		"P1: guard must reference the workflow_run event name to gate the safety check")
	assert.True(t, strings.Contains(condition, "repository.id"),
		"P1: guard must compare github.event.workflow_run.repository.id against repository_id")
	assert.True(t, strings.Contains(condition, "repository_id"),
		"P1: guard must reference github.repository_id as the comparison target")
	assert.True(t, strings.Contains(condition, "fork"),
		"P1: guard must check repository.fork to reject forked-repo workflow_run triggers")
	assert.True(t, strings.HasPrefix(condition, "${{") && strings.HasSuffix(condition, "}}"),
		"P1: rendered condition must be wrapped in GitHub Actions expression syntax")
}

func TestFormalPM005_WorkflowRunRepoSafetyOnlyWhenTriggerDeclared(t *testing.T) {
	t.Parallel()

	c := workflow.NewCompiler()

	tests := []struct {
		name        string
		frontmatter map[string]any
		wantTrigger bool
	}{
		{
			name:        "nil frontmatter has no workflow_run trigger",
			frontmatter: nil,
			wantTrigger: false,
		},
		{
			name:        "empty frontmatter has no workflow_run trigger",
			frontmatter: map[string]any{},
			wantTrigger: false,
		},
		{
			name: "map-form on.workflow_run is detected",
			frontmatter: map[string]any{
				"on": map[string]any{"workflow_run": map[string]any{"workflows": []any{"CI"}}},
			},
			wantTrigger: true,
		},
		{
			name: "string-form on: workflow_run is detected",
			frontmatter: map[string]any{
				"on": "workflow_run",
			},
			wantTrigger: false, // string form equality check requires exact match; other triggers absent here intentionally documents current behavior
		},
		{
			name: "unrelated trigger (push) is not workflow_run",
			frontmatter: map[string]any{
				"on": map[string]any{"push": map[string]any{}},
			},
			wantTrigger: false,
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := c.HasWorkflowRunTriggerForTest(tc.frontmatter)
			assert.Equal(t, tc.wantTrigger, got,
				"P2: hasWorkflowRunTrigger(%v) must equal %v", tc.frontmatter, tc.wantTrigger)
		})
	}
}

// ---------------------------------------------------------------------------
// P3: workflow_run.workflows non-empty field requirement
// ---------------------------------------------------------------------------

func TestFormalPM_WorkflowRunRequiresNonEmptyWorkflows(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		value any
		valid bool
	}{
		{"non-empty string", "CI", true},
		{"empty string", "", false},
		{"whitespace-only string", "   ", false},
		{"non-empty []string", []string{"CI"}, true},
		{"empty []string", []string{}, false},
		{"non-empty []any with string", []any{"CI"}, true},
		{"[]any with only empty strings", []any{"", "  "}, false},
		{"nil value", nil, false},
		{"unsupported type (int)", 42, false},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := workflow.HasNonEmptyWorkflowRunWorkflowsForTest(tc.value)
			assert.Equal(t, tc.valid, got,
				"P3: hasNonEmptyWorkflowRunWorkflows(%v) must equal %v", tc.value, tc.valid)
		})
	}
}

// ---------------------------------------------------------------------------
// P4 / T-PM-003: strict-mode gating of missing branch restrictions
// ---------------------------------------------------------------------------

func TestFormalPM003_StrictModeGatesMissingBranchRestriction(t *testing.T) {
	t.Parallel()

	onWithoutBranches := `on:
  workflow_run:
    workflows: ["CI"]
    types: [completed]
`
	onWithBranches := `on:
  workflow_run:
    workflows: ["CI"]
    types: [completed]
    branches: [main]
`

	tests := []struct {
		name      string
		strict    bool
		on        string
		wantError bool
	}{
		{
			name:      "strict mode + missing branches => compile error",
			strict:    true,
			on:        onWithoutBranches,
			wantError: true,
		},
		{
			name:      "non-strict mode + missing branches => warning only, no error",
			strict:    false,
			on:        onWithoutBranches,
			wantError: false,
		},
		{
			name:      "strict mode + branches present => no error",
			strict:    true,
			on:        onWithBranches,
			wantError: false,
		},
		{
			name:      "non-strict mode + branches present => no error",
			strict:    false,
			on:        onWithBranches,
			wantError: false,
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			c := workflow.NewCompiler()
			c.SetStrictMode(tc.strict)

			data := &workflow.WorkflowData{On: tc.on}
			err := c.ValidateWorkflowRunBranchesForTest(data, "test.md")

			if tc.wantError {
				require.Error(t, err,
					"P4/T-PM-003: strict mode must reject workflow_run trigger missing branch restrictions")
			} else {
				assert.NoError(t, err,
					"P4/T-PM-003: non-strict mode (or satisfied branches) must not produce a hard error")
			}
		})
	}
}

func TestFormalPM_BranchRestrictionPresentIsNoOp(t *testing.T) {
	t.Parallel()

	c := workflow.NewCompiler()
	c.SetStrictMode(true)

	data := &workflow.WorkflowData{On: `on:
  workflow_run:
    workflows: ["CI"]
    branches: [main, develop]
`}

	err := c.ValidateWorkflowRunBranchesForTest(data, "test.md")
	assert.NoError(t, err,
		"P5: a workflow_run trigger with branch restrictions present must never produce an error, even in strict mode")
}

func TestFormalPM_NoWorkflowRunTriggerSkipsValidation(t *testing.T) {
	t.Parallel()

	c := workflow.NewCompiler()
	c.SetStrictMode(true)

	data := &workflow.WorkflowData{On: `on:
  push:
    branches: [main]
`}

	err := c.ValidateWorkflowRunBranchesForTest(data, "test.md")
	assert.NoError(t, err,
		"P6: workflows without a workflow_run trigger must skip validation entirely, regardless of strict mode")
}

// ---------------------------------------------------------------------------
// P7-P9 / T-PM-007: GitHub token precedence and role-based chain isolation
// ---------------------------------------------------------------------------

func TestFormalPM007_DefaultGitHubTokenPrecedence(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		custom     string
		wantResult string
	}{
		{
			name:       "custom token takes precedence",
			custom:     "${{ secrets.MY_CUSTOM_TOKEN }}",
			wantResult: "${{ secrets.MY_CUSTOM_TOKEN }}",
		},
		{
			name:       "empty custom token falls back to default chain",
			custom:     "",
			wantResult: "${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}",
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := workflow.GetEffectiveGitHubTokenForTest(tc.custom)
			assert.Equal(t, tc.wantResult, got,
				"P7/T-PM-007: getEffectiveGitHubToken(%q) must equal %q", tc.custom, tc.wantResult)
		})
	}
}

func TestFormalPM007_SafeOutputTokenPrecedence(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		custom     string
		wantResult string
	}{
		{
			name:       "custom safe-output token takes precedence",
			custom:     "${{ secrets.MY_SAFE_OUTPUT_TOKEN }}",
			wantResult: "${{ secrets.MY_SAFE_OUTPUT_TOKEN }}",
		},
		{
			name:       "empty custom token falls back to safe-output default chain",
			custom:     "",
			wantResult: "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}",
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := workflow.GetEffectiveSafeOutputGitHubTokenForTest(tc.custom)
			assert.Equal(t, tc.wantResult, got,
				"P8/T-PM-007: getEffectiveSafeOutputGitHubToken(%q) must equal %q", tc.custom, tc.wantResult)
		})
	}
}

func TestFormalPM007_TokenChainsDifferByRole(t *testing.T) {
	t.Parallel()

	toolChain := workflow.GetEffectiveGitHubTokenForTest("")
	safeOutputChain := workflow.GetEffectiveSafeOutputGitHubTokenForTest("")

	require.NotEqual(t, toolChain, safeOutputChain,
		"P9/T-PM-007: tool-token default chain and safe-output default chain must be distinct fallback expressions")
	assert.True(t, strings.Contains(toolChain, "GH_AW_GITHUB_MCP_SERVER_TOKEN"),
		"P9: tool/agent default token chain must include the MCP-server-specific secret")
	assert.False(t, strings.Contains(safeOutputChain, "GH_AW_GITHUB_MCP_SERVER_TOKEN"),
		"P9: safe-output default token chain must NOT include the MCP-server-specific secret (write-scope isolation)")
	assert.True(t, strings.Contains(safeOutputChain, "GITHUB_TOKEN"),
		"P9: safe-output default chain must still fall back to the built-in GITHUB_TOKEN")
}

// ---------------------------------------------------------------------------
// P10 / T-PM-003: SetStrictMode is a deterministic, idempotent setter
// ---------------------------------------------------------------------------

func TestFormalPM003_SetStrictModeIsIdempotentSetter(t *testing.T) {
	t.Parallel()

	c := workflow.NewCompiler()

	c.SetStrictMode(true)
	require.True(t, c.StrictModeForTest(), "P10: SetStrictMode(true) must set strictMode to true")

	c.SetStrictMode(true)
	require.True(t, c.StrictModeForTest(), "P10: repeated SetStrictMode(true) must remain idempotent")

	c.SetStrictMode(false)
	require.False(t, c.StrictModeForTest(), "P10: SetStrictMode(false) must reset strictMode to false")
}

// ---------------------------------------------------------------------------
// P11: HasBashExplicitRestriction boundary matrix (edge cases, carried from
// prior compiler-threat-detection formalization; re-verified as a supporting
// engine-capability compile-time gate analogous to T-PM-003's strict gating)
// ---------------------------------------------------------------------------

func TestFormalPM_BashExplicitRestrictionBoundary(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		tools          map[string]any
		wantRestricted bool
	}{
		{
			name:           "nil tools map must not panic and is unrestricted",
			tools:          nil,
			wantRestricted: false,
		},
		{
			name:           "absent bash key is unrestricted",
			tools:          map[string]any{},
			wantRestricted: false,
		},
		{
			name:           "bash: nil present-key is treated as absent (unrestricted)",
			tools:          map[string]any{"bash": nil},
			wantRestricted: false,
		},
		{
			name:           "bash: true is unrestricted",
			tools:          map[string]any{"bash": true},
			wantRestricted: false,
		},
		{
			name:           "bash: false is an explicit restriction",
			tools:          map[string]any{"bash": false},
			wantRestricted: true,
		},
		{
			name:           "bash: [] empty list is an explicit restriction",
			tools:          map[string]any{"bash": []any{}},
			wantRestricted: true,
		},
		{
			name:           `bash: ["*"] wildcard is unrestricted`,
			tools:          map[string]any{"bash": []any{"*"}},
			wantRestricted: false,
		},
		{
			name:           `bash: [":*"] wildcard is unrestricted`,
			tools:          map[string]any{"bash": []any{":*"}},
			wantRestricted: false,
		},
		{
			name:           "bash: [named commands] is an explicit restriction",
			tools:          map[string]any{"bash": []any{"echo", "ls"}},
			wantRestricted: true,
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := workflow.HasBashExplicitRestriction(tc.tools)
			assert.Equal(t, tc.wantRestricted, got,
				"P11: HasBashExplicitRestriction(%v) must equal %v", tc.tools, tc.wantRestricted)
		})
	}
}

Usage

  1. Copy the test file to pkg/workflow/security_architecture_pm_formal_test.go.
  2. Some helpers referenced here (HasWorkflowRunTriggerForTest, BuildWorkflowRunRepoSafetyConditionForTest, ValidateWorkflowRunBranchesForTest, StrictModeForTest, HasNonEmptyWorkflowRunWorkflowsForTest, GetEffectiveGitHubTokenForTest, GetEffectiveSafeOutputGitHubTokenForTest) wrap currently-unexported package internals (hasWorkflowRunTrigger, buildWorkflowRunRepoSafetyCondition, validateWorkflowRunBranches, strictMode, hasNonEmptyWorkflowRunWorkflows, getEffectiveGitHubToken, getEffectiveSafeOutputGitHubToken). Either add small _test.go-only exported wrappers inside package workflow, or move this suite into package workflow (non-_test variant) instead of workflow_test to access the unexported symbols directly. HasBashExplicitRestriction and NewCompiler/SetStrictMode are already exported and usable as-is from workflow_test.
  3. Run: go test ./pkg/workflow/... -run Formal

Context

Generated by 🔬 Daily Formal Spec Verifier · auto · 65.1 AIC · ⌖ 25.9 AIC · ⊞ 9.9K ·

  • expires on Aug 19, 2026, 7:52 AM UTC-08:00

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions