Skip to content

Resolve artifact bucket from merged tree, not the untrusted PR head - #152

Merged
maxknv merged 15 commits into
mainfrom
fixes_for_merge-commit
Sep 30, 2026
Merged

maxknv merged 15 commits into
mainfrom
fixes_for_merge-commit

Conversation

@maxknv

@maxknv maxknv commented Sep 29, 2026 •

Copy link
Copy Markdown
Member
  1. enable s3 redirect for zot docker proxy, as serving blobs from the host is suboptimal
  2. support pining controller and praktika versions via ssm ci_config parameter (test and dev mode). allows to switch praktika version w/o infra reaaply and pins versions accross ci run
  3. hardenings

maxknv and others added 2 commits September 29, 2026 12:35
The controller read S3_ARTIFACT_BUCKET from the PR head tree before the
ephemeral merge. A PR head lacking the project's private settings override
(e.g. an upstream-sync branch) resolved the wrong bucket, so the snapshot
upload hit AccessDenied (IAM correctly refused the off-scope write).

Fix: in force-merge mode read the bucket from the merged tree (base + head)
after the merge, the same tree the reinstalled runtime uses, and disable the
sticky merge base so nothing needs the bucket before the merge.

- merge.read_repo_settings: force_merge_commit now also forces
  STICKY_MERGE_BASE_HOURS to 0.
- merge.prepare_repo_snapshot: re-read the bucket from the merged clone and
  publish to it.
- Document the sticky-base limitation in docs/ci-config.md.
- Bump praktika 0.1.13 to 0.1.14, praktika-controller 0.1.7 to 0.1.8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@praktika-gh

praktika-gh Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Workflow [Praktika CI Advanced], commit [4b49a42]

Summary: ✅


Code Review

Result: ❌ Blocking issues

What changed: Adds SSM-backed Praktika/controller version pinning, controller self-updates, JSONC config support, and runtime venv cache handling. It also changes snapshot creation/upload and merged-tree bucket resolution, improves workflow-load failure reporting, and adds Docker proxy and infrastructure deployment enhancements.

Blocking issues

  • Controller self-update can enter an endless reinstall/restart cycle when its state cannot be persisted.
  • Operational SSM read failures are treated as an absent configuration, allowing a run to silently bypass configured version pins and other controls.
  • Reopened the existing relative-path controller-pin thread because the validation was removed while the documentation still says relative paths are rejected.

The previously reported self-update result handling, mutable local-runtime cache, and workflow-load check issues are fixed in the current code.

Investigation: 6/13 rounds, 36 tool calls.

…toml ve

Update _HEAD_PRAKTIKA_VERSION to match the current pyproject.toml version (0.1.14) so the Version Check job's assertion passes.

AI-Session-Round: 9bba16ab004c
Comment thread bootstrap/src/praktika_controller/merge.py Outdated
Comment thread bootstrap/src/praktika_controller/controller.py Outdated
Comment thread bootstrap/src/praktika_controller/controller.py Outdated
Comment thread bootstrap/src/praktika_controller/self_update.py Outdated
Comment thread bootstrap/src/praktika_controller/controller.py
@maxknv
maxknv force-pushed the fixes_for_merge-commit branch from 60f2bdf to e9df0c2 Compare September 30, 2026 09:31
MAX_ATTEMPTS,
)
try:
_pip_install(desired_source, python=python, run=run)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking: The documented filesystem/repo-path form cannot work here. maybe_self_update runs immediately after receiving the message, before the workflow repository is cloned, and _pip_install passes a relative source such as . or ./bootstrap directly to pip. It therefore resolves against the systemd process's working directory rather than the run checkout, repeatedly fails, and eventually processes the message under the old controller after hitting the attempt cap. Either resolve/prepare the checkout before installation or restrict controller pins to version specs, immutable URLs, and absolute host paths.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in fa7e22d. Controller pins are now validated up front (_is_valid_controller_source): only a URL, a requirement spec, or an absolute host path are accepted. A relative path (., ./bootstrap) is rejected cleanly with an error log and no install attempt (so it never burns the attempt cap), since self-update runs before any checkout exists. Documented in ci-config.md; praktika_version still resolves relative paths against the run checkout as before. Added test_relative_path_pin_is_rejected_without_install.

maxknv and others added 2 commits September 30, 2026 11:45
- Read ci_config once per received message and pass the snapshot into
  handle_workflow, so the controller self-update decision and the value
  frozen into run metadata come from the same read (no split on a
  mid-message SSM change).
- Run controller self-update inside a VisibilityHeartbeat and release the
  message only after the heartbeat stops, so a slow download+pip install
  cannot let the message be picked up concurrently.
- Give the fresh-instance self-update a best-effort rollback target (the
  running version by spec); note the in-place install atomicity gap as
  deferred production hardening.
- Key ensure_praktika_venv's cache by the install source so a changed
  praktika_version pin actually reinstalls instead of reusing a stale venv.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Controller self-update runs before any workflow checkout exists, so a
relative repo-path praktika_controller_version (".", "./bootstrap") can't
be resolved and would loop until the attempt cap. Restrict controller
pins to version specs, URLs, or absolute host paths and reject a relative
path cleanly (no install attempt). praktika_version keeps resolving
relative paths against the run checkout as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pin = _controller_version_pin(ci_config)
if pin:
with VisibilityHeartbeat(sqs, queue_url, receipt, visibility):
did_self_update = maybe_self_update(pin, log)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking: False from maybe_self_update does not only mean “already at the pin”; it also means an invalid source, an exhausted attempt cap, an install failure, or a failed import/rollback. Every one of those cases falls through and processes this message under the old controller, despite its frozen praktika_controller_version. A bad or temporarily unavailable pin can therefore split one run across controller versions. Return a distinct failure state (or raise) and release/fail the message without processing it; continue only when the desired source is confirmed satisfied.

maxknv and others added 3 commits September 30, 2026 12:06
load_ci_config now strips // and /* */ comments and trailing commas
before json.loads, so an operator can comment out a field without a
strict parse error silently reading the whole parameter as {} (every
feature off). The stripper is string-aware: a // inside an https:// URL
or a comma inside a string value is preserved.

Also lock in that an empty-string version pin is treated as unset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deploy() now seeds the out-of-repo CI config parameter with a
commented-out JSONC template (parses to {}, every feature off) that
documents all supported keys with short descriptions. Create-if-absent
only (ParameterAlreadyExists is ignored) so a redeploy never clobbers an
operator's live value. Gated by _wants("CIConfig"), so a full deploy or
`--only CIConfig` creates it. Destroy already sweeps it by prefix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The base workflow-orchestrator runs the AMI-baked praktika (pinned to
_PRAKTIKA_BASE_VERSION), while the repo's workflow files can move ahead.
One file using a newer feature (e.g. ci/workflows/ignition_dispatch.py's
Workflow.Engine.GH_IGNITION, absent in 0.1.9) raised AttributeError at
import inside _get_workflows(), crashing the whole scan -> orchestrate
rc=100 -> InfraOrchestrationError -> the workflow never started and the
"CI" check was left stuck in_progress.

1) Bump _PRAKTIKA_BASE_VERSION 0.1.9 -> 0.1.13 (latest published wheel
   that has GH_IGNITION; 0.1.14 is not published yet). Requires a base
   AMI rebake + base ASG roll.

2) Harden _get_workflows(): a single workflow file that fails to import
   is skipped with a warning instead of crashing the scan, so the other
   workflows still load and match. Validation (_for_validation_check)
   re-raises so `praktika validate`, run under the checkout's own
   praktika, still catches genuinely broken files.

3) Surface skipped files in GitHub checks: load errors are threaded out
   of _get_workflows -> find_workflows_for_event -> the orchestrator,
   which posts a completed "Workflow load error: <file>" failure check on
   the PR head so a skipped workflow is never silently lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread praktika/orchestrator/__init__.py
Comment thread bootstrap/src/praktika_controller/self_update.py Outdated
Comment thread bootstrap/src/praktika_controller/merge.py
…tity

r4143720081: when EVERY workflow file fails to import, _get_workflows()
previously hit its `if not res: raise` before returning, so load errors
were never surfaced and the bootstrap check was left in_progress. Now it
returns empty when load errors were collected; the orchestrator posts the
per-file "Workflow load error" checks and finalizes the bootstrap check as
failure instead of neutral.

r4143720089: self-update verification only proved *some* praktika-controller
imports, not that desired_source installed the intended distribution. A
typo like `praktika==0.1.8` or a URL to an unrelated wheel would install,
read back the old version, and be persisted as converged. Now the pin's
project identity is validated before install (from the == spec name or the
wheel filename) and, for an exact pin, the installed version must match
after install before it is adopted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maxknv
maxknv force-pushed the fixes_for_merge-commit branch from e653a62 to a43819d Compare September 30, 2026 12:01
Comment thread bootstrap/src/praktika_controller/venv_manager.py
The "already converged" and "already at pinned version" short-circuits
returned silently, so a matching praktika_controller_version pin looked
like nothing happened (no log line between RECEIVED and Processing). Log
both at INFO so an operator can confirm the pin was read and honored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread praktika/orchestrator/__init__.py Outdated
maxknv and others added 3 commits September 30, 2026 14:33
Per PR review r4143720089: this is a dev-mode mechanism, so drop the
identity/version validation, post-install verification, rollback, and
crash-loop counter. maybe_self_update now: no-op on empty/already-pinned
source (persisted), else pip install --force-reinstall and restart. A
bad/unreachable pin fails hard (pip's error propagates) instead of being
validated or recovered. State keeps only installed_source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per PR review r4144394537: keying the no-base_venv cache by the source
string still served stale Praktika for a mutable repo/filesystem pin
(e.g. praktika_version: "."), because the same path with different
content across runs computes the same key and the cached venv is reused
as soon as `import praktika` succeeds. Now a cached venv is reused as-is
only for passthrough (immutable) sources — a URL or version spec; for a
local-path source it is reinstalled with --force-reinstall --no-deps on
every task, mirroring the base-venv overlay, so a run never executes a
stale checkout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per PR review r4144514665: CheckRun (orchestrator/check_run.py) has no
create_completed() — that helper is on state.py's check class — so the
call raised AttributeError, swallowed by the surrounding except, and the
failure check was never posted (a sibling workflow's bootstrap check could
even go green). Use the existing two-call API: start(... with_cancel_action
=False).complete("failure", output=...). Added a regression test that the
load-error check is actually posted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
log.info("Self-updating controller -> %s", desired_source)
_pip_install(desired_source, python=python, run=run) # fail hard on a bad pin
state["installed_source"] = desired_source
_save_state(state, log)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking: _save_state swallows every write failure, but this still returns True and restarts the process. On restart, the old or missing state again differs from desired_source, so the controller reinstalls, releases the message, and restarts forever. Because each iteration exits cleanly, the poll loop's infra-failure replacement path is never reached. Persist the converged source durably before signaling restart, and propagate a persistence failure so the instance is retried/replaced.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

_save_state is in the dev mode branch. acceptable

# controller self-update decision here and (for a workflow) the value
# handle_workflow freezes into run metadata — never re-read SSM, or a
# mid-message change could split the two.
ci_config = _ci_config_for_message(role, payload, log)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking: load_ci_config converts every SSM failure—including timeouts, access errors, and malformed values—to {}. This call therefore interprets a transient failure as an explicit absence of the controller/runtime pins and processes the workflow under whatever versions happen to be installed, freezing the empty config into all job tasks. That defeats the pin precisely when SSM is temporarily unavailable. Treat only a genuinely missing optional parameter as {}; operational/read/parse failures should abort this message and enter the normal infra-retry path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fine

@maxknv
maxknv merged commit f0281a1 into main Sep 30, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant