diff --git a/.github/workflows/auto-tag-runtime.yml b/.github/workflows/auto-tag-runtime.yml deleted file mode 100644 index 5ba8257d..00000000 --- a/.github/workflows/auto-tag-runtime.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: auto-tag-runtime - -# Auto-tag runtime releases on every merge to main that touches workspace/. -# This is the entry point of the runtime CD chain: -# -# merge PR → auto-tag-runtime (this) → publish-runtime → cascade → template -# image rebuilds → repull on hosts. -# -# Default bump is patch. Override via PR label `release:minor` or -# `release:major` BEFORE merging — the label is read off the merged PR -# associated with the push commit. -# -# Skips when: -# - The push isn't to main (other branches don't auto-release). -# - The merge commit message contains `[skip-release]` (escape hatch -# for cleanup PRs that touch workspace/ but shouldn't ship). - -on: - push: - branches: [main] - paths: - - "workspace/**" - - "scripts/build_runtime_package.py" - - ".github/workflows/auto-tag-runtime.yml" - - ".github/workflows/publish-runtime.yml" - -permissions: - contents: write # to push the new tag - pull-requests: read # to read labels off the merged PR - -concurrency: - # Serialize tag bumps so two near-simultaneous merges can't both think - # they're 0.1.6 and race to push the same tag. - group: auto-tag-runtime - cancel-in-progress: false - -jobs: - tag: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 # need full tag history for `git describe` / sort - - - name: Skip when commit asks - id: skip - run: | - MSG=$(git log -1 --format=%B "${{ github.sha }}") - if echo "$MSG" | grep -qiE '\[skip-release\]|\[no-release\]'; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "Commit message contains [skip-release] — no tag will be created." - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Determine bump kind from PR label - id: bump - if: steps.skip.outputs.skip != 'true' - env: - # Gitea-shape token (act_runner forwards GITHUB_TOKEN as a - # short-lived per-run secret with read access to this repo). - # We hit `/api/v1/repos/.../pulls?state=closed` directly - # because `gh pr list` calls Gitea's GraphQL endpoint, which - # returns HTTP 405 (issue #75 / post-#66 sweep). - GITEA_TOKEN: ${{ github.token }} - REPO: ${{ github.repository }} - GITEA_API_URL: ${{ github.server_url }}/api/v1 - PUSH_SHA: ${{ github.sha }} - run: | - # Find the merged PR whose merge_commit_sha matches this push. - # Gitea's `/repos/{owner}/{repo}/pulls?state=closed` returns - # PRs sorted newest-first; we paginate up to 50 and jq-filter - # on `merge_commit_sha == PUSH_SHA`. Bounded — auto-tag fires - # per push to main, so the matching PR is always among the - # most recent closures. 50 is comfortably more than the - # ~10-20 staging→main promotes that close in any reasonable - # window. - set -euo pipefail - PRS_JSON=$(curl --fail-with-body -sS \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Accept: application/json" \ - "${GITEA_API_URL}/repos/${REPO}/pulls?state=closed&sort=newest&limit=50" \ - 2>/dev/null || echo "[]") - PR=$(printf '%s' "$PRS_JSON" \ - | jq -c --arg sha "$PUSH_SHA" \ - '[.[] | select(.merged_at != null and .merge_commit_sha == $sha)] | .[0] // empty') - if [ -z "$PR" ] || [ "$PR" = "null" ]; then - echo "No merged PR found for ${PUSH_SHA} — defaulting to patch bump." - echo "kind=patch" >> "$GITHUB_OUTPUT" - exit 0 - fi - # Gitea returns labels under `.labels[].name`, same shape as - # GitHub's REST. The previous `gh pr list --json number,labels` - # output was identical; jq filter unchanged. - LABELS=$(printf '%s' "$PR" | jq -r '.labels[]?.name // empty') - if echo "$LABELS" | grep -qx 'release:major'; then - echo "kind=major" >> "$GITHUB_OUTPUT" - elif echo "$LABELS" | grep -qx 'release:minor'; then - echo "kind=minor" >> "$GITHUB_OUTPUT" - else - echo "kind=patch" >> "$GITHUB_OUTPUT" - fi - - - name: Compute next version from latest runtime-v* tag - id: version - if: steps.skip.outputs.skip != 'true' - run: | - # Find the highest runtime-vX.Y.Z tag. `sort -V` handles semver - # ordering; `grep` filters to the right tag prefix. - LATEST=$(git tag --list 'runtime-v*' | sort -V | tail -1) - if [ -z "$LATEST" ]; then - # No prior tag — start the runtime line at 0.1.0. - CURRENT="0.0.0" - else - CURRENT="${LATEST#runtime-v}" - fi - MAJOR=$(echo "$CURRENT" | cut -d. -f1) - MINOR=$(echo "$CURRENT" | cut -d. -f2) - PATCH=$(echo "$CURRENT" | cut -d. -f3) - case "${{ steps.bump.outputs.kind }}" in - major) MAJOR=$((MAJOR+1)); MINOR=0; PATCH=0;; - minor) MINOR=$((MINOR+1)); PATCH=0;; - patch) PATCH=$((PATCH+1));; - esac - NEW="$MAJOR.$MINOR.$PATCH" - echo "current=$CURRENT" >> "$GITHUB_OUTPUT" - echo "new=$NEW" >> "$GITHUB_OUTPUT" - echo "Bumping runtime $CURRENT → $NEW (${{ steps.bump.outputs.kind }})" - - - name: Push new tag - if: steps.skip.outputs.skip != 'true' - run: | - NEW_TAG="runtime-v${{ steps.version.outputs.new }}" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag -a "$NEW_TAG" -m "runtime $NEW_TAG (auto-bump from ${{ steps.bump.outputs.kind }})" - git push origin "$NEW_TAG" - echo "Pushed $NEW_TAG — publish-runtime workflow will fire on the tag." diff --git a/.github/workflows/branch-protection-drift.yml b/.github/workflows/branch-protection-drift.yml deleted file mode 100644 index 2a782405..00000000 --- a/.github/workflows/branch-protection-drift.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: branch-protection drift check - -# Catches out-of-band edits to branch protection (UI clicks, manual gh -# api PATCH from a one-off ops session) by comparing live state against -# tools/branch-protection/apply.sh's desired state every day. Fails the -# workflow when they drift; the failure is the signal. -# -# When it fails: re-run apply.sh to put the live state back to the -# script's intent, OR update apply.sh to encode the new intent and -# commit. Either way the script is the source of truth. - -on: - schedule: - # 14:00 UTC daily. Off-hours for most teams; gives a fresh signal - # at the start of every working day. - - cron: '0 14 * * *' - workflow_dispatch: - pull_request: - branches: [staging, main] - paths: - - 'tools/branch-protection/**' - - '.github/workflows/**' - - '.github/workflows/branch-protection-drift.yml' - -permissions: - contents: read - -jobs: - drift: - name: Branch protection drift - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # Token strategy by trigger: - # - # - schedule (daily canary): hard-fail when the admin token is - # missing. This is the *only* trigger where silent soft-skip is - # dangerous — a missing secret on the cron run means the drift - # gate has effectively disappeared with no human in the loop to - # notice. Per feedback_schedule_vs_dispatch_secrets_hardening.md - # the rule is "schedule/automated triggers must hard-fail". - # - # - pull_request (touching tools/branch-protection/**): soft-skip - # with a prominent warning. A PR cannot retroactively drift the - # live state — drift happens *between* PRs (UI clicks, manual - # gh api PATCH) and is the schedule's job to catch. The PR-time - # gate would only catch typos in apply.sh, which the apply.sh - # *_payload unit tests catch better. A human is reviewing the - # PR and will see the warning in the workflow log. - # - # - workflow_dispatch (operator one-off): soft-skip with warning, - # so an operator can run a diagnostic without configuring the - # secret first. - - name: Verify admin token present (hard-fail on schedule only) - env: - GH_TOKEN_FOR_ADMIN_API: ${{ secrets.GH_TOKEN_FOR_ADMIN_API }} - run: | - if [[ -n "$GH_TOKEN_FOR_ADMIN_API" ]]; then - echo "GH_TOKEN_FOR_ADMIN_API present — drift_check will run with admin scope." - exit 0 - fi - if [[ "${{ github.event_name }}" == "schedule" ]]; then - echo "::error::GH_TOKEN_FOR_ADMIN_API secret missing on the daily canary." >&2 - echo "" >&2 - echo "The schedule run is the SoT for branch-protection drift detection." >&2 - echo "Without admin scope it silently passes, hiding any out-of-band edits." >&2 - echo "Set GH_TOKEN_FOR_ADMIN_API at Settings → Secrets and variables → Actions." >&2 - exit 1 - fi - echo "::warning::GH_TOKEN_FOR_ADMIN_API secret missing — drift_check will be SKIPPED." - echo "::warning::PR drift checks need repo-admin scope to read /branches/:b/protection." - echo "::warning::This is non-fatal: the daily schedule run is the canonical drift gate." - echo "SKIP_DRIFT_CHECK=1" >> "$GITHUB_ENV" - - - name: Run drift check - if: env.SKIP_DRIFT_CHECK != '1' - env: - # Repo-admin scope, needed for /branches/:b/protection. - GH_TOKEN: ${{ secrets.GH_TOKEN_FOR_ADMIN_API }} - run: bash tools/branch-protection/drift_check.sh - - # Self-test the parity script before running it on the real - # workflows — pins the script's classification logic against - # synthetic safe/unsafe/missing/unsafe-mix/matrix fixtures so a - # regression in the script can't false-pass on the production - # workflow audit. Cheap (~0.5s); always runs. - - name: Self-test check-name parity script - run: bash tools/branch-protection/test_check_name_parity.sh - - # Check-name parity gate (#144 / saved memory - # feedback_branch_protection_check_name_parity). - # - # drift_check.sh asserts the live branch protection matches what - # apply.sh would set; check_name_parity.sh closes the orthogonal - # gap: it asserts every required check name in apply.sh maps to a - # workflow job whose "always emits this status" shape is intact. - # - # The two checks fail in different scenarios: - # - # - drift_check fails → live state was rewritten out-of-band - # (UI click, manual PATCH). - # - check_name_parity fails → an apply.sh required name has no - # emitter, OR the emitting workflow has a top-level paths: - # filter without per-step if-gates (the silent-block shape). - # - # Cheap (~1s); runs without the admin token because it only reads - # apply.sh + .github/workflows/ from the checkout. - - name: Run check-name parity gate - run: bash tools/branch-protection/check_name_parity.sh diff --git a/.github/workflows/check-merge-group-trigger.yml b/.github/workflows/check-merge-group-trigger.yml deleted file mode 100644 index 7d65a526..00000000 --- a/.github/workflows/check-merge-group-trigger.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Check merge_group trigger on required workflows - -# Pre-merge guard against the deadlock pattern where a workflow whose -# check is in `required_status_checks` lacks a `merge_group:` trigger. -# Without it, GitHub merge queue stalls forever in AWAITING_CHECKS -# because the required check can't fire on `gh-readonly-queue/...` refs. -# -# This workflow: -# 1. Lists required status checks on the branch protection rule for `staging` -# 2. For each required check, finds the workflow that produces it (by job -# name match) -# 3. Fails if any such workflow lacks `merge_group:` in its triggers -# -# Reasoning for staging-only: main has its own CI gating model (PR review), -# but staging is what the merge queue runs on, so it's the trigger that -# matters. -# -# Gitea stub: Gitea has no merge queue feature and no `merge_group:` -# event type. The linter would find no `merge_group:` triggers to verify -# (they don't exist on Gitea), so the lint is vacuously satisfied. -# Converting to a no-op stub keeps the workflow+job name stable for any -# commit-status context consumers while eliminating the `gh api` call -# that fails against Gitea's REST surface (#75 / PR-D). - -on: - pull_request: - paths: - - '.github/workflows/**.yml' - - '.github/workflows/**.yaml' - push: - branches: [staging, main] - paths: - - '.github/workflows/**.yml' - - '.github/workflows/**.yaml' - -jobs: - check: - name: Required workflows have merge_group trigger - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Gitea no-op (merge queue not applicable) - run: | - echo "Gitea Actions — merge queue not supported; no-op." - echo "On GitHub this workflow lints that required-check workflows declare" - echo "merge_group: triggers to prevent queue deadlock. On Gitea that" - echo "constraint is inapplicable — all workflows pass vacuously." diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index dec301a6..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: CodeQL - -# Stub workflow — CodeQL Action is structurally incompatible with Gitea -# Actions (post-2026-05-06 SCM migration off GitHub). -# -# Why this is a stub, not a real CodeQL run: -# -# 1. github/codeql-action/init@v4 hits api.github.com endpoints -# (CodeQL CLI bundle download + query-pack registry + telemetry) -# that Gitea 1.22.x does NOT proxy. The act_runner has -# GITHUB_SERVER_URL=https://git.moleculesai.app correctly set -# (per saved memory feedback_act_runner_github_server_url and -# /config.yaml on the operator host), but the Gitea API surface -# simply does not implement the codeql-action bundle endpoints. -# Observed in run 1d/3101 (2026-05-07): "::error::404 page not -# found" inside the Initialize CodeQL step, before any analysis. -# -# 2. PR #35 attempted to mark `continue-on-error: true` at the JOB -# level (correct YAML structure). Gitea 1.22.6 does NOT propagate -# job-level continue-on-error to the commit-status API — every -# matrix leg still posts `failure` to the status surface, which -# keeps OVERALL=failure on every push to main + staging and -# blocks visual auto-promote signals (#156). -# -# 3. Hongming policy decision (2026-05-07, task #156): CodeQL is -# ADVISORY, not blocking, on Gitea Actions. We do not block PR -# merge or staging→main promotion on CodeQL findings until we -# have a Gitea-compatible static-analysis pipeline. -# -# What this stub preserves: -# -# - Workflow name `CodeQL` (referenced by auto-promote-staging.yml -# line 67 as a workflow_run gate — must stay stable). -# - Job name template `Analyze (${{ matrix.language }})` and the -# 3-leg matrix (go, javascript-typescript, python). Branch -# protection / required-check parity (#144) keys on these -# exact context names. -# - merge_group + push + pull_request + schedule triggers, so the -# merge-queue check name still resolves (per saved memory -# feedback_branch_protection_check_name_parity). -# -# Re-enabling real analysis (future work): -# -# - Option A: self-hosted Semgrep / OpenGrep via a custom action -# that doesn't hit api.github.com. Tracked behind #156 follow-up. -# - Option B: Sonatype Nexus IQ or similar, called from a step -# that uses the Gitea-issued token only. -# - Option C: re-host this workflow on a small GitHub mirror used -# ONLY for SAST (push-mirrored from Gitea). Acceptable trade-off -# if/when payment is restored on a non-suspended GitHub org — -# but per saved memory feedback_no_single_source_of_truth, we -# should design for multi-vendor backup, not GitHub-only SAST. -# -# Until one of those lands, this stub keeps commit-status green so -# the auto-promote chain isn't permanently red on a tool we cannot -# actually run. -# -# Security policy: ADVISORY. We accept the residual risk of un-scanned -# pushes during this window. Compensating controls in place: -# - secret-scan.yml runs on every push (active, blocks on hits) -# - block-internal-paths.yml blocks forbidden file paths -# - lint-curl-status-capture.yml catches one specific class of bug -# - branch-protection-drift.yml + the merge_group required-checks -# parity keep the gate surface stable -# These are not equivalent to CodeQL coverage. Status of the -# replacement plan is tracked in #156. - -on: - push: - branches: [main, staging] - pull_request: - branches: [main, staging] - # Required so the matrix legs emit a real result on the queued - # commit instead of a false-green when merge queue is enabled. - # Per saved memory feedback_branch_protection_check_name_parity: - # path-filtered / matrix workflows MUST emit the protected name - # via a job that always runs. - merge_group: - types: [checks_requested] - schedule: - # Weekly heartbeat. Cheap on a stub (the no-op job is ~5s) but - # keeps the workflow visible in Gitea's Actions UI so the next - # operator notices it's a stub instead of a missing surface. - - cron: '30 1 * * 0' - -# Workflow-level concurrency: only one stub run per branch/PR at a -# time. cancel-in-progress: false because a quick follow-up push -# shouldn't kill an in-flight run — even though the stub is fast, -# the contract should match a real CodeQL run for when we re-enable. -concurrency: - group: codeql-${{ github.ref }} - cancel-in-progress: false - -permissions: - actions: read - contents: read - # No security-events: write — we don't call the upload API anyway, - # GHAS isn't on Gitea. - -jobs: - analyze: - # Job NAME shape is load-bearing — auto-promote-staging.yml + - # branch protection both key on `Analyze (${{ matrix.language }})`. - # Do NOT rename without coordinating both surfaces. - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - timeout-minutes: 5 - - strategy: - fail-fast: false - matrix: - language: [go, javascript-typescript, python] - - steps: - # Single-step stub: log the policy decision + emit success. - # Exit 0 explicitly so the commit-status API records `success` - # for each of the three matrix legs. - - name: CodeQL stub (advisory, non-blocking on Gitea) - shell: bash - run: | - set -euo pipefail - cat <> "$GITHUB_OUTPUT" - echo "::notice::Gitea Actions detected — auto-merge gating is not applicable here (Gitea has no --auto merge primitive). Job will no-op." - else - echo "is_gitea=false" >> "$GITHUB_OUTPUT" - fi - - - name: Disable auto-merge (GitHub only) - if: steps.host.outputs.is_gitea != 'true' - env: - GH_TOKEN: ${{ github.token }} - PR: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} - NEW_SHA: ${{ github.sha }} - run: | - set -eu - gh pr merge "$PR" --disable-auto -R "$REPO" || true - gh pr comment "$PR" -R "$REPO" --body "🔒 Auto-merge disabled — new commit (\`${NEW_SHA:0:7}\`) pushed after auto-merge was enabled. The merge queue locks SHAs at entry, so subsequent pushes can race. Verify the new commit and re-enable with \`gh pr merge --auto\`." - - - name: Gitea no-op - if: steps.host.outputs.is_gitea == 'true' - run: echo "Gitea Actions — auto-merge gating not applicable; no-op (job intentionally green so branch protection's required-check name lands SUCCESS)." diff --git a/.github/workflows/promote-latest.yml b/.github/workflows/promote-latest.yml deleted file mode 100644 index e16027c3..00000000 --- a/.github/workflows/promote-latest.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: promote-latest - -# Manually retag ghcr.io/molecule-ai/platform:staging- → :latest -# (and the same for the tenant image). Use this to: -# -# 1. Promote a :staging- to prod before the canary fleet is live -# (one-off during the initial rollout). -# 2. Roll back :latest to a prior known-good digest after a bad -# promotion slipped past canary (use scripts/rollback-latest.sh -# for a local / emergency path; this workflow is for scheduled -# or from-browser promotions). -# -# Running this workflow needs no extra secrets — GitHub's default -# GITHUB_TOKEN has write:packages for repo-owned GHCR images, which -# is all we need for a remote retag via `crane tag`. - -on: - workflow_dispatch: - inputs: - sha: - description: 'Short sha to promote (e.g. 4c1d56e). Must match an existing :staging- tag.' - required: true - type: string - -permissions: - contents: read - packages: write - -env: - IMAGE_NAME: ghcr.io/molecule-ai/platform - TENANT_IMAGE_NAME: ghcr.io/molecule-ai/platform-tenant - -jobs: - promote: - runs-on: ubuntu-latest - steps: - - uses: imjasonh/setup-crane@6da1ae018866400525525ce74ff892880c099987 # v0.5 - - - name: GHCR login - run: | - echo "${{ secrets.GITHUB_TOKEN }}" \ - | crane auth login ghcr.io -u "${{ github.actor }}" --password-stdin - - - name: Retag platform image - run: | - set -eu - SRC="${IMAGE_NAME}:staging-${{ inputs.sha }}" - if ! crane digest "$SRC" >/dev/null 2>&1; then - echo "::error::$SRC not found in registry — double-check the sha." - exit 1 - fi - EXPECTED=$(crane digest "$SRC") - crane tag "$SRC" latest - ACTUAL=$(crane digest "${IMAGE_NAME}:latest") - if [ "$ACTUAL" != "$EXPECTED" ]; then - echo "::error::retag digest mismatch (expected $EXPECTED, got $ACTUAL)" - exit 1 - fi - echo "OK ${IMAGE_NAME}:latest → $ACTUAL" - - - name: Retag tenant image - run: | - set -eu - SRC="${TENANT_IMAGE_NAME}:staging-${{ inputs.sha }}" - if ! crane digest "$SRC" >/dev/null 2>&1; then - echo "::error::$SRC not found — tenant image may not have built for this sha." - exit 1 - fi - EXPECTED=$(crane digest "$SRC") - crane tag "$SRC" latest - ACTUAL=$(crane digest "${TENANT_IMAGE_NAME}:latest") - if [ "$ACTUAL" != "$EXPECTED" ]; then - echo "::error::tenant retag digest mismatch" - exit 1 - fi - echo "OK ${TENANT_IMAGE_NAME}:latest → $ACTUAL" - - - name: Summary - run: | - { - echo "## :latest promoted to staging-${{ inputs.sha }}" - echo - echo "Both platform + tenant images retagged. Prod tenants" - echo "will auto-pull within their 5-min update cycle." - } >> "$GITHUB_STEP_SUMMARY" diff --git a/runbooks/gitea-actions-migration-checklist.md b/runbooks/gitea-actions-migration-checklist.md new file mode 100644 index 00000000..dd87d0c5 --- /dev/null +++ b/runbooks/gitea-actions-migration-checklist.md @@ -0,0 +1,112 @@ +# Gitea Actions migration checklist (molecule-core) + +Created 2026-05-11 as part of **RFC `molecule-ai/internal#219` §1** — the +sweep of `.github/workflows/*.yml` files in `molecule-core` after the +2026-05-06 GitHub → Gitea migration. Documents which workflows were +retired, which were ported, and the reasoning for each. + +The sweep used the four-surface audit pattern from saved memory +`feedback_gitea_actions_migration_audit_pattern`: + +1. **YAML** — drop `workflow_dispatch.inputs`, `merge_group`, + `environment:`. Adjust `runs-on:`. Set `env.GITHUB_SERVER_URL` + per `feedback_act_runner_github_server_url`. +2. **Cache** — verify `actions/cache@v4` / `upload-artifact` pin + compatibility with Gitea 1.22.x runner. +3. **Token** — auto-injected `GITHUB_TOKEN` works for same-repo + operations; cross-repo dispatch needs explicit secret. +4. **Docs** — top-of-file "Ported from .github/workflows/X.yml on + YYYY-MM-DD per RFC internal#219 §1 sweep" comment. + +Per RFC §1 contract, all ports land with `continue-on-error: true` on +every job to surface bugs without blocking; a follow-up PR flips +`continue-on-error: false` after triage. + +## Category A — already mirrored (deleted .github/ copy) + +These workflows had a working `.gitea/workflows/X.yml` twin at the time +of the sweep. The `.github/` copies were silently dead (Gitea Actions +in molecule-core only registers `.gitea/workflows/`) and have been +removed. + +| File | .gitea/ twin | +|---|---| +| `publish-runtime.yml` | `.gitea/workflows/publish-runtime.yml` (ported via issue #206) | +| `secret-scan.yml` | `.gitea/workflows/secret-scan.yml` | + +## Category B — GitHub-only, retired + +These workflows depend on GitHub-specific surface (merge queue, GitHub +auto-merge primitive, github.com REST API, GHCR registry, CodeQL action +that hits api.github.com bundle endpoints) that Gitea does not provide. +No equivalent Gitea-side workflow is needed; the underlying mechanism +either doesn't exist on Gitea or has been replaced by a different +pipeline. + +| File | Why retired | +|---|---| +| `auto-tag-runtime.yml` | Superseded by `.gitea/workflows/publish-runtime-autobump.yml` (auto-bump-on-workspace-edit). The autobump only does patch bumps; the deleted workflow supported `release:minor` / `release:major` PR-label-driven bumps. Follow-up issue should track restoring label-driven minor/major if anyone uses it. | +| `branch-protection-drift.yml` | Targets `Molecule-AI/molecule-core` on GitHub via `gh api /repos/.../branch-protection` — entirely GitHub-API specific. `tools/branch-protection/drift_check.sh` and `apply.sh` reference the GitHub schema (status_check_contexts, dismiss_stale_reviews, etc.) which differs from Gitea's `branch_protections` shape. Rebuilding for Gitea is out of scope for the RFC #219 sweep; follow-up issue needed for Gitea-compatible branch-protection drift detection. | +| `check-merge-group-trigger.yml` | The workflow's own header (lines 18-23) documents that it's vacuously satisfied on Gitea — Gitea has no merge queue, no `merge_group:` event type, no `gh-readonly-queue/...` refs. Nothing to lint. | +| `codeql.yml` | The workflow's own header (lines 3-67) documents that `github/codeql-action/init@v4` hits api.github.com bundle endpoints not implemented by Gitea (observed: `::error::404 page not found` in Initialize CodeQL step). Per Hongming decision 2026-05-07 (task #156): CodeQL is ADVISORY/non-blocking until a Gitea-compatible SAST pipeline lands. Replacement options (Semgrep self-host, Sonatype, GitHub-mirror-for-SAST) tracked in #156. | +| `pr-guards.yml` | The workflow's own header documents that Gitea has no `gh pr merge --auto` primitive — the guard is a structural no-op on Gitea. Branch protection on `main` does NOT reference any `pr-guards` check name; deletion is safe. | +| `promote-latest.yml` | Uses `imjasonh/setup-crane` against `ghcr.io/molecule-ai/platform` — the GHCR registry was retired during the 2026-05-06 Gitea migration (per `canary-verify.yml` header notes, the canonical tenant image moved to ECR `153263036946.dkr.ecr.us-east-2.amazonaws.com/molecule-ai/platform-tenant`). The workflow can no longer find any image to retag. Follow-up issue suggested if an ECR-based retag promote is desired. | + +## Category C — ported to .gitea/ + +These workflows had real ongoing CI value but no Gitea-side equivalent. +Each was ported to `.gitea/workflows/X.yml` with: + +- `workflow_dispatch.inputs` removed (Gitea 1.22.6 parser rejects them — + per `feedback_gitea_workflow_dispatch_inputs_unsupported`) +- `merge_group:` trigger removed (no merge queue) +- `environment:` blocks removed (Gitea has no environments) +- `dorny/paths-filter@v4` replaced with inline `git diff` (per the + pattern established in PR#372 ci.yml port) +- `env.GITHUB_SERVER_URL: https://git.moleculesai.app` set at workflow + level (belt-and-suspenders for `actions/checkout` etc.) +- `continue-on-error: true` on every job (RFC §1 contract — surface + defects without blocking; follow-up PR flips after triage) +- Top-of-file header: "Ported from .github/workflows/X.yml on + YYYY-MM-DD per RFC internal#219 §1 sweep." + +See the C-1 / C-2 / C-3 sweep PRs for the file lists and per-file +adjustments. + +## Category D — parser-rejected (none for molecule-core) + +The RFC #219 §1 brief lists 7 workflows as parser-rejected (`audit-orphan-instances`, +`bake-thin-ami`, `bench-provision-time`, `cache-probe`, `deploy-pipeline`, +`e2e-tunnel-reboot`, `persona-author-check`). Verification against +molecule-core's tree (and the `docker logs molecule-gitea-1` parser-rejection +log) shows these workflows belong to other repos: + +- `audit-orphan-instances`, `bake-thin-ami`, `bench-provision-time`, + `deploy-pipeline`, `e2e-tunnel-reboot` live in `molecule-ai/molecule-controlplane` +- `cache-probe`, `persona-author-check` live in `molecule-ai/internal` + +For molecule-core, **Category D is empty**. + +## Verification + +After all sweep PRs land: + +```bash +# Should produce nothing. +ls .github/workflows/*.yml | grep -vF ci.yml + +# Should list 6 working workflows from the .gitea/ port directory + the +# C-1/C-2/C-3 ports. +ls .gitea/workflows/*.yml +``` + +Gitea Actions server should produce NO `[W] ignore invalid workflow` +lines for any `.gitea/workflows/X.yml` in molecule-core when commits +land on `main`: + +```bash +ssh root@5.78.80.188 'docker logs molecule-gitea-1 --since 10m 2>&1 \ + | grep "ignore invalid workflow" \ + | grep -i molecule-core' +# Expected: empty. +```