Repository navigation
feat: standardize CI workflows and branch protection [OMN-7648] #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
549eef8
feat: add standard CI workflows and branch protection gates [OMN-7648]
jonahgabriel 72cbe8f
ci: checkout omnibase_core for schema compat CI [OMN-7648]
jonahgabriel 92c23c1
fix(ci): use git clone for omnibase_core in schema compat workflow
jonahgabriel 9f30922
fix(ci): install omnibase_spi for schema compat check
jonahgabriel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # OMN-6571: Automatically enable auto-merge (squash) on every PR opened | ||
| # by jonahgabriel. Ensures PRs do not sit idle in the merge queue waiting | ||
| # for manual enrollment. | ||
| name: Auto-Merge | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, reopened, ready_for_review] | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| auto-merge: | ||
| name: Enable Auto-Merge | ||
| runs-on: ubuntu-latest | ||
| if: github.actor == 'jonahgabriel' | ||
| steps: | ||
| # continue-on-error: merge queue re-triggers this workflow, and | ||
| # gh pr merge fails with "already enqueued" — that is expected. | ||
| - name: Enable auto-merge (squash) | ||
| continue-on-error: true | ||
| run: gh pr merge "${{ github.event.pull_request.number }}" --auto --squash | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| # Contract Validation Gate | ||
| # | ||
| # Validates ONEX ticket contracts for PRs in omnimarket using the | ||
| # reusable composite action from onex_change_control. | ||
| # | ||
| # The validation is intentionally lenient on missing contracts: | ||
| # - If branch name has no OMN-XXXX ticket ID -> skipped | ||
| # - If contracts/<ticket_id>.yaml does not exist -> skipped | ||
| # - If contract file exists but fails schema validation -> fails | ||
|
|
||
| name: Contract Validation | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [main] | ||
| merge_group: | ||
| workflow_dispatch: | ||
| inputs: | ||
| branch-name: | ||
| description: "Branch name to validate contracts for (defaults to current branch)" | ||
| required: false | ||
| default: "" | ||
|
|
||
| concurrency: | ||
| group: contract-validation-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| contract-validation: | ||
| name: contract-validation | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
|
|
||
| steps: | ||
| - name: Auth preflight — confirm onex_change_control action is accessible | ||
| id: auth-preflight | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| echo "Checking visibility of OmniNode-ai/onex_change_control..." | ||
| if gh api repos/OmniNode-ai/onex_change_control --silent; then | ||
| echo "::notice::onex_change_control repo is accessible." | ||
| else | ||
| echo "::error::Cannot reach OmniNode-ai/onex_change_control. Composite action will not resolve." | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Resolve branch name | ||
| id: resolve-branch | ||
| env: | ||
| WORKFLOW_INPUT_BRANCH: ${{ inputs.branch-name }} | ||
| GITHUB_HEAD_REF: ${{ github.head_ref }} | ||
| GITHUB_REF_NAME: ${{ github.ref_name }} | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| run: | | ||
| if [[ "$EVENT_NAME" == "merge_group" ]]; then | ||
| echo "resolved_branch=" >> "$GITHUB_OUTPUT" | ||
| echo "skip_validation=true" >> "$GITHUB_OUTPUT" | ||
| echo "::notice::merge_group event — contract validation skipped (validated on PR branch instead)." | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "skip_validation=false" >> "$GITHUB_OUTPUT" | ||
|
|
||
| if [[ -n "$WORKFLOW_INPUT_BRANCH" ]]; then | ||
| BRANCH="$WORKFLOW_INPUT_BRANCH" | ||
| elif [[ -n "$GITHUB_HEAD_REF" ]]; then | ||
| BRANCH="$GITHUB_HEAD_REF" | ||
| else | ||
| BRANCH="$GITHUB_REF_NAME" | ||
| fi | ||
| echo "resolved_branch=$BRANCH" >> "$GITHUB_OUTPUT" | ||
| echo "::notice::Resolved branch name: $BRANCH" | ||
|
|
||
| - name: Run contract validation | ||
| id: validate-contract | ||
| if: steps.resolve-branch.outputs.skip_validation != 'true' | ||
| uses: OmniNode-ai/onex_change_control/.github/actions/validate-contract@main | ||
| with: | ||
| branch-name: ${{ steps.resolve-branch.outputs.resolved_branch }} | ||
| contracts-path: contracts | ||
|
|
||
| - name: Report validation result | ||
| if: always() | ||
| env: | ||
| TICKET_ID: ${{ steps.validate-contract.outputs.ticket-id }} | ||
| VALIDATION_STATUS: ${{ steps.validate-contract.outputs.validation-status }} | ||
| SKIP_VALIDATION: ${{ steps.resolve-branch.outputs.skip_validation }} | ||
| run: | | ||
| if [[ "$SKIP_VALIDATION" == "true" ]]; then | ||
| echo "::notice::Contract validation SKIPPED — merge_group event." | ||
| exit 0 | ||
| fi | ||
| case "$VALIDATION_STATUS" in | ||
| passed) | ||
| echo "::notice::Contract validation PASSED for ticket $TICKET_ID" | ||
| ;; | ||
| skipped) | ||
| if [[ -z "$TICKET_ID" ]]; then | ||
| echo "::notice::Contract validation SKIPPED — no OMN-XXXX ticket ID found in branch name." | ||
| else | ||
| echo "::notice::Contract validation SKIPPED — no contract file found for $TICKET_ID in contracts/." | ||
| fi | ||
| ;; | ||
| failed) | ||
| echo "::error::Contract validation FAILED for ticket $TICKET_ID. See validate-contract step for details." | ||
| exit 1 | ||
| ;; | ||
| *) | ||
| echo "::warning::Unexpected validation status: '$VALIDATION_STATUS'. Treating as skipped." | ||
| ;; | ||
|
jonahgabriel marked this conversation as resolved.
|
||
| esac | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| # SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| # linear-close-on-merge.yml | ||
| # | ||
| # Auto-close Linear tickets when their associated PR merges. | ||
| # | ||
| # Fires on pull_request closed events. When the PR was actually merged | ||
| # (not just closed), extracts the OMN-XXXX ticket ID from the branch name | ||
| # and calls the Linear API to mark the ticket as Done. | ||
| # | ||
| # Branch name conventions supported: | ||
| # jonahgabriel/omn-XXXX-description (Linear default) | ||
| # jonah/omn-XXXX-description (short username variant) | ||
| # epic/OMN-XXXX/OMN-YYYY/run-id (epic-team branches, closes child OMN-YYYY) | ||
| # OMN-XXXX-description (bare prefix variant) | ||
| # | ||
| # Implements OMN-3262. | ||
|
|
||
| name: Close Linear Ticket on PR Merge | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [closed] | ||
| branches: | ||
| - main | ||
| - develop | ||
|
|
||
| jobs: | ||
| close-linear-ticket: | ||
| name: Close Linear Ticket | ||
| if: github.event.pull_request.merged == true | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Extract ticket ID from branch name | ||
| id: extract | ||
| env: | ||
| BRANCH: ${{ github.event.pull_request.head.ref }} | ||
| shell: bash | ||
| run: | | ||
| echo "Branch: $BRANCH" | ||
|
|
||
| TICKET_ID="" | ||
|
|
||
| # Try to extract from epic branch: epic/OMN-XXXX/OMN-YYYY/... | ||
| if printf '%s' "$BRANCH" | grep -qiE "^epic/OMN-[0-9]+/(OMN-[0-9]+)(/|$)"; then | ||
| TICKET_ID=$(printf '%s' "$BRANCH" | grep -ioE "^epic/OMN-[0-9]+/(OMN-[0-9]+)(/|$)" | grep -ioE "OMN-[0-9]+" | tail -1) | ||
| fi | ||
|
|
||
| # Try standard branch: */omn-XXXX-* or */OMN-XXXX-* | ||
| if [[ -z "$TICKET_ID" ]]; then | ||
| TICKET_ID=$(printf '%s' "$BRANCH" | grep -ioE "(OMN|omn)-[0-9]+" | head -1 | tr '[:lower:]' '[:upper:]') | ||
| fi | ||
|
|
||
| if [[ -z "$TICKET_ID" ]]; then | ||
| echo "No OMN-XXXX ticket ID found in branch '$BRANCH' — skipping." | ||
| echo "ticket_id=" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "Extracted ticket ID: $TICKET_ID" | ||
| echo "ticket_id=$TICKET_ID" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Close Linear ticket | ||
| if: steps.extract.outputs.ticket_id != '' | ||
| env: | ||
| LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} | ||
| TICKET_ID: ${{ steps.extract.outputs.ticket_id }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| REPO: ${{ github.repository }} | ||
| shell: bash | ||
| run: | | ||
| if [ -z "$LINEAR_API_KEY" ]; then | ||
| echo "LINEAR_API_KEY is not set — skipping Linear ticket close" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Validate LINEAR_API_KEY format (alphanumeric only) | ||
| if ! printf '%s' "$LINEAR_API_KEY" | grep -qE '^[A-Za-z0-9_-]+$'; then | ||
| echo "LINEAR_API_KEY appears malformed — skipping Linear ticket close" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Validate extracted IDs are safe for JSON interpolation | ||
| if ! echo "$TICKET_ID" | grep -qE '^(OMN|omn)-[0-9]+$'; then | ||
| echo "Invalid TICKET_ID format: $TICKET_ID — skipping" | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "Marking $TICKET_ID as Done (PR #$PR_NUMBER merged in $REPO)" | ||
|
|
||
| SEARCH_RESPONSE=$(curl -s --max-time 30 -X POST https://api.linear.app/graphql \ | ||
| -H "Authorization: $LINEAR_API_KEY" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{\"query\": \"{ issue(id: \\\"$TICKET_ID\\\") { id identifier title state { name } } }\"}" \ | ||
| 2>&1) | ||
|
|
||
| echo "Linear API search: $(echo "$SEARCH_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print('found' if d.get('data',{}).get('issue') else 'not found')" 2>/dev/null || echo "status-unknown")" | ||
|
|
||
| ISSUE_ID=$(echo "$SEARCH_RESPONSE" | python3 -c " | ||
| import json, sys | ||
| data = json.load(sys.stdin) | ||
| issue = data.get('data', {}).get('issue') | ||
| if issue: | ||
| print(issue['id']) | ||
| else: | ||
| print('') | ||
| " 2>/dev/null || echo "") | ||
|
|
||
| if [ -z "$ISSUE_ID" ]; then | ||
| echo "Could not extract issue ID from Linear response — skipping" | ||
| exit 0 | ||
| fi | ||
|
|
||
| if ! echo "$ISSUE_ID" | grep -qE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then | ||
| echo "ISSUE_ID is not a valid UUID: $ISSUE_ID — skipping" | ||
| exit 0 | ||
| fi | ||
|
|
||
| CURRENT_STATE=$(echo "$SEARCH_RESPONSE" | python3 -c " | ||
| import json, sys | ||
| data = json.load(sys.stdin) | ||
| issue = data.get('data', {}).get('issue') | ||
| if issue: | ||
| print(issue.get('state', {}).get('name', '')) | ||
| else: | ||
| print('') | ||
| " 2>/dev/null || echo "") | ||
|
|
||
| echo "Issue $TICKET_ID (ID: $ISSUE_ID) current state: $CURRENT_STATE" | ||
|
|
||
| if echo "$CURRENT_STATE" | grep -qiE "^(done|cancelled|duplicate|completed|merged|released|shipped|closed)$"; then | ||
| echo "$TICKET_ID is already in state '$CURRENT_STATE' — no update needed." | ||
| exit 0 | ||
| fi | ||
|
|
||
| TEAM_RESPONSE=$(curl -s --max-time 30 -X POST https://api.linear.app/graphql \ | ||
| -H "Authorization: $LINEAR_API_KEY" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{\"query\": \"{ issue(id: \\\"$TICKET_ID\\\") { team { id states { nodes { id name type } } } } }\"}" \ | ||
| 2>&1) | ||
|
|
||
| DONE_STATE_ID=$(echo "$TEAM_RESPONSE" | python3 -c " | ||
| import json, sys | ||
| data = json.load(sys.stdin) | ||
| states = (data.get('data', {}) | ||
| .get('issue', {}) | ||
| .get('team', {}) | ||
| .get('states', {}) | ||
| .get('nodes', [])) | ||
| completed = [s for s in states if s.get('type', '').upper() == 'COMPLETED'] | ||
| done = next((s for s in completed if s.get('name', '').lower() == 'done'), None) | ||
| if done: | ||
| print(done['id']) | ||
| elif completed: | ||
| print(sorted(completed, key=lambda s: s.get('name', ''))[0]['id']) | ||
| " 2>/dev/null || echo "") | ||
|
|
||
| if [ -z "$DONE_STATE_ID" ]; then | ||
| echo "Could not find Done state ID for this team — skipping" | ||
| exit 0 | ||
| fi | ||
|
|
||
| if ! echo "$DONE_STATE_ID" | grep -qE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then | ||
| echo "DONE_STATE_ID is not a valid UUID: $DONE_STATE_ID — skipping" | ||
| exit 0 | ||
| fi | ||
|
|
||
| UPDATE_RESPONSE=$(curl -s --max-time 30 -X POST https://api.linear.app/graphql \ | ||
| -H "Authorization: $LINEAR_API_KEY" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{\"query\": \"mutation { issueUpdate(id: \\\"$ISSUE_ID\\\", input: { stateId: \\\"$DONE_STATE_ID\\\" }) { success issue { id state { name } } } }\"}" \ | ||
| 2>&1) | ||
|
|
||
| SUCCESS=$(echo "$UPDATE_RESPONSE" | python3 -c " | ||
| import json, sys | ||
| data = json.load(sys.stdin) | ||
| print(data.get('data', {}).get('issueUpdate', {}).get('success', False)) | ||
| " 2>/dev/null || echo "False") | ||
|
|
||
| if [[ "$SUCCESS" == "True" ]]; then | ||
| echo "Successfully marked $TICKET_ID as Done." | ||
| else | ||
| echo "WARNING: Failed to mark $TICKET_ID as Done." | ||
| exit 0 | ||
| fi |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.