)]}'
{"/COMMIT_MSG":[{"author":{"_account_id":10342,"name":"Jay Faulkner","display_name":"JayF","email":"jay@jvf.cc","username":"JayF","status":"youtube.com/@oss-gr / podcast.gr-oss.io"},"change_message_id":"c9a28552722cad9be7361bed75d240a899083a8a","unresolved":true,"context_lines":[{"line_number":36,"context_line":"- Update release_mappings.py master entry to api 1.112 and add"},{"line_number":37,"context_line":"  RunbookTrait/RunbookTraitList objects."},{"line_number":38,"context_line":"- Include a spec document at specs/approved/runbook-traits.rst"},{"line_number":39,"context_line":"  describing the design."},{"line_number":40,"context_line":""},{"line_number":41,"context_line":"Assisted-by: Claude Sonnet 4.6"},{"line_number":42,"context_line":"Change-Id: Ifc21e37a257f4749170e2802324f868185cb1886"}],"source_content_type":"text/x-gerrit-commit-message","patch_set":7,"id":"00cd488e_330c3f81","line":39,"updated":"2026-04-08 20:48:31.000000000","message":"This is weird to have here at the end, referencing another repo. Maybe needs a human pass on the whole commit message?\n\nAlso, this is a pretty large change and it could likely benefit from being split up. I\u0027m going to review it from here ignoring this specific piece of feedback but we *really* as a community need to get better with PR sizing.","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"411b19a20d545c9b474164d86a0edd7e109a0c8a","unresolved":false,"context_lines":[{"line_number":36,"context_line":"- Update release_mappings.py master entry to api 1.112 and add"},{"line_number":37,"context_line":"  RunbookTrait/RunbookTraitList objects."},{"line_number":38,"context_line":"- Include a spec document at specs/approved/runbook-traits.rst"},{"line_number":39,"context_line":"  describing the design."},{"line_number":40,"context_line":""},{"line_number":41,"context_line":"Assisted-by: Claude Sonnet 4.6"},{"line_number":42,"context_line":"Change-Id: Ifc21e37a257f4749170e2802324f868185cb1886"}],"source_content_type":"text/x-gerrit-commit-message","patch_set":7,"id":"62be1cbf_44d350a7","line":39,"in_reply_to":"00cd488e_330c3f81","updated":"2026-04-08 21:38:19.000000000","message":"Done","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"}],"/PATCHSET_LEVEL":[{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"e68d2672679dfc2e629b772c59030596f830b925","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":3,"id":"668be565_5b0d5b5b","updated":"2026-04-04 17:37:09.000000000","message":"recheck github.com 504\u0027d","commit_id":"8529de71eb9eef021b80c2cf7023e463ac3e8225"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"5070ba1e40134e34354168e87b1a6a57f8657cfe","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":6,"id":"6943a133_96201e9a","updated":"2026-04-08 17:31:47.000000000","message":"A couple of nits, the code looks sane so far. Will continue tomorrow.","commit_id":"b0f09795c0b1c3aa76385f4fac8545b2dab4ca25"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"05452e924c04818aeb4742d6f4e8265dae5d69d7","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":10,"id":"19b76288_edfdb1bb","updated":"2026-04-09 17:35:27.000000000","message":"A question/suggestion on the API design (around PATCH) inline. Also a bunch of nits.","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":4571,"name":"Steve Baker","email":"sbaker@redhat.com","username":"steve-stevebaker"},"change_message_id":"f92b64182c6c2ef44e6d90d7832457d52754538b","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":11,"id":"608bf06a_46d15741","updated":"2026-04-13 02:32:33.000000000","message":"Looks good, just a question about legacy behaviour","commit_id":"fc74fb01ec94f25d2d5df6e10c8e39b370e5f6fd"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"bd6d0e1480c0eb86898f350d52dfd46ead0e2600","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":12,"id":"1383ac5a_a7db8846","updated":"2026-04-13 17:08:13.000000000","message":"recheck tests failed due to https://review.opendev.org/c/openstack/ironic/+/982706","commit_id":"88ae5713654e7829f751f9b7c2ea895b5d505fd6"}],"doc/source/contributor/webapi-version-history.rst":[{"author":{"_account_id":10342,"name":"Jay Faulkner","display_name":"JayF","email":"jay@jvf.cc","username":"JayF","status":"youtube.com/@oss-gr / podcast.gr-oss.io"},"change_message_id":"c9a28552722cad9be7361bed75d240a899083a8a","unresolved":true,"context_lines":[{"line_number":7,"context_line":""},{"line_number":8,"context_line":"Add runbook traits support:"},{"line_number":9,"context_line":""},{"line_number":10,"context_line":"* Runbooks now carry a ``traits`` set (list of valid trait strings)."},{"line_number":11,"context_line":"  When executing a runbook on a node, the system checks that the node"},{"line_number":12,"context_line":"  has at least one trait that intersects with the runbook\u0027s traits set."},{"line_number":13,"context_line":"  This replaces the previous requirement that the runbook ``name``"}],"source_content_type":"text/x-rst","patch_set":7,"id":"54fe58ed_943d0baf","line":10,"updated":"2026-04-08 20:48:31.000000000","message":"nit: custom trait strings","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"411b19a20d545c9b474164d86a0edd7e109a0c8a","unresolved":false,"context_lines":[{"line_number":7,"context_line":""},{"line_number":8,"context_line":"Add runbook traits support:"},{"line_number":9,"context_line":""},{"line_number":10,"context_line":"* Runbooks now carry a ``traits`` set (list of valid trait strings)."},{"line_number":11,"context_line":"  When executing a runbook on a node, the system checks that the node"},{"line_number":12,"context_line":"  has at least one trait that intersects with the runbook\u0027s traits set."},{"line_number":13,"context_line":"  This replaces the previous requirement that the runbook ``name``"}],"source_content_type":"text/x-rst","patch_set":7,"id":"72b481e5_45666d2d","line":10,"in_reply_to":"54fe58ed_943d0baf","updated":"2026-04-08 21:38:19.000000000","message":"Done","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"05452e924c04818aeb4742d6f4e8265dae5d69d7","unresolved":true,"context_lines":[{"line_number":16,"context_line":"  digits, hyphens, dots, underscores; 1–255 characters). The old"},{"line_number":17,"context_line":"  constraint that names must follow the trait format (``CUSTOM_*`` or"},{"line_number":18,"context_line":"  an ``os-traits`` standard trait) is lifted."},{"line_number":19,"context_line":"* A ``description`` field (nullable string, max 255 characters) is added"},{"line_number":20,"context_line":"  to runbooks, consistent with other Ironic objects."},{"line_number":21,"context_line":"* New sub-resource ``/v1/runbooks/{ident}/traits`` with endpoints:"},{"line_number":22,"context_line":""}],"source_content_type":"text/x-rst","patch_set":10,"id":"ade5154e_e10330b9","line":19,"updated":"2026-04-09 17:35:27.000000000","message":"nit: I think this should be listed separately, not under \"traits support\".","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"ca114f8a694e7050d9786ab57b9eb5d384fc50e3","unresolved":false,"context_lines":[{"line_number":16,"context_line":"  digits, hyphens, dots, underscores; 1–255 characters). The old"},{"line_number":17,"context_line":"  constraint that names must follow the trait format (``CUSTOM_*`` or"},{"line_number":18,"context_line":"  an ``os-traits`` standard trait) is lifted."},{"line_number":19,"context_line":"* A ``description`` field (nullable string, max 255 characters) is added"},{"line_number":20,"context_line":"  to runbooks, consistent with other Ironic objects."},{"line_number":21,"context_line":"* New sub-resource ``/v1/runbooks/{ident}/traits`` with endpoints:"},{"line_number":22,"context_line":""}],"source_content_type":"text/x-rst","patch_set":10,"id":"b3c7b683_d10f911a","line":19,"in_reply_to":"ade5154e_e10330b9","updated":"2026-04-10 03:36:47.000000000","message":"Done","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"}],"ironic/api/controllers/v1/runbook.py":[{"author":{"_account_id":37208,"name":"Marek Skrobacki","display_name":"Marek Skrobacki","email":"skrobul@skrobul.com","username":"skrobul"},"change_message_id":"37ec9307a1f3e018a9ed893fe2d81406655467b8","unresolved":true,"context_lines":[{"line_number":254,"context_line":"        if body and \u0027traits\u0027 in body:"},{"line_number":255,"context_line":"            traits \u003d body[\u0027traits\u0027]"},{"line_number":256,"context_line":""},{"line_number":257,"context_line":"        if (trait and traits is not None) or not (trait or traits is not None):"},{"line_number":258,"context_line":"            msg \u003d _(\"A single runbook trait may be added via PUT \""},{"line_number":259,"context_line":"                    \"/v1/runbooks/\u003crunbook identifier\u003e/traits/\u003ctrait\u003e with \""},{"line_number":260,"context_line":"                    \"no body, or all runbook traits may be replaced via PUT \""}],"source_content_type":"text/x-python","patch_set":4,"id":"6bd11b20_5cd8cf83","line":257,"updated":"2026-04-08 15:28:33.000000000","message":"```suggestion\n        has_single \u003d bool(trait)\n        has_list \u003d traits is not None\n\n        if has_single \u003d\u003d has_list:\n```","commit_id":"36c6b9b5ed53dadc5cbcb416b8499c1ca853d386"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"da468628f5cae7baf19120900bbb39810dd1da0d","unresolved":false,"context_lines":[{"line_number":254,"context_line":"        if body and \u0027traits\u0027 in body:"},{"line_number":255,"context_line":"            traits \u003d body[\u0027traits\u0027]"},{"line_number":256,"context_line":""},{"line_number":257,"context_line":"        if (trait and traits is not None) or not (trait or traits is not None):"},{"line_number":258,"context_line":"            msg \u003d _(\"A single runbook trait may be added via PUT \""},{"line_number":259,"context_line":"                    \"/v1/runbooks/\u003crunbook identifier\u003e/traits/\u003ctrait\u003e with \""},{"line_number":260,"context_line":"                    \"no body, or all runbook traits may be replaced via PUT \""}],"source_content_type":"text/x-python","patch_set":4,"id":"7abf0ed7_e8872d7a","line":257,"in_reply_to":"6bd11b20_5cd8cf83","updated":"2026-04-08 16:47:54.000000000","message":"Done","commit_id":"36c6b9b5ed53dadc5cbcb416b8499c1ca853d386"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"5070ba1e40134e34354168e87b1a6a57f8657cfe","unresolved":true,"context_lines":[{"line_number":69,"context_line":""},{"line_number":70,"context_line":"# Schema for API version \u003e\u003d 1.112: name may be any logical string, and"},{"line_number":71,"context_line":"# a \u0027traits\u0027 list is accepted."},{"line_number":72,"context_line":"RUNBOOK_SCHEMA_V112 \u003d {"},{"line_number":73,"context_line":"    \u0027type\u0027: \u0027object\u0027,"},{"line_number":74,"context_line":"    \u0027properties\u0027: {"},{"line_number":75,"context_line":"        \u0027uuid\u0027: {\u0027type\u0027: [\u0027string\u0027, \u0027null\u0027]},"}],"source_content_type":"text/x-python","patch_set":6,"id":"30d124f2_ea586fef","line":72,"updated":"2026-04-08 17:31:47.000000000","message":"nit: copy RUNBOOK_SCHEMA and update only the fields you need","commit_id":"b0f09795c0b1c3aa76385f4fac8545b2dab4ca25"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"79d18f3bab70b4f3da220211979d9329804ccc47","unresolved":false,"context_lines":[{"line_number":69,"context_line":""},{"line_number":70,"context_line":"# Schema for API version \u003e\u003d 1.112: name may be any logical string, and"},{"line_number":71,"context_line":"# a \u0027traits\u0027 list is accepted."},{"line_number":72,"context_line":"RUNBOOK_SCHEMA_V112 \u003d {"},{"line_number":73,"context_line":"    \u0027type\u0027: \u0027object\u0027,"},{"line_number":74,"context_line":"    \u0027properties\u0027: {"},{"line_number":75,"context_line":"        \u0027uuid\u0027: {\u0027type\u0027: [\u0027string\u0027, \u0027null\u0027]},"}],"source_content_type":"text/x-python","patch_set":6,"id":"3639688a_3b242be7","line":72,"in_reply_to":"30d124f2_ea586fef","updated":"2026-04-08 18:28:04.000000000","message":"Done","commit_id":"b0f09795c0b1c3aa76385f4fac8545b2dab4ca25"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"5070ba1e40134e34354168e87b1a6a57f8657cfe","unresolved":true,"context_lines":[{"line_number":456,"context_line":"        # For pre-v1.112 requests, traits are not in the schema so they won\u0027t"},{"line_number":457,"context_line":"        # be present. For v1.112+ they may be provided."},{"line_number":458,"context_line":"        if not api_utils.allow_runbook_traits():"},{"line_number":459,"context_line":"            runbook.pop(\u0027traits\u0027, None)"},{"line_number":460,"context_line":""},{"line_number":461,"context_line":"        # Ensure traits is always set so the notification payload can be"},{"line_number":462,"context_line":"        # populated (RunbookCRUDPayload.SCHEMA includes \u0027traits\u0027)."}],"source_content_type":"text/x-python","patch_set":6,"id":"ef41f0c6_a3f05fdf","line":459,"updated":"2026-04-08 17:31:47.000000000","message":"nit: I don\u0027t think this does much in combination with line 463","commit_id":"b0f09795c0b1c3aa76385f4fac8545b2dab4ca25"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"79d18f3bab70b4f3da220211979d9329804ccc47","unresolved":false,"context_lines":[{"line_number":456,"context_line":"        # For pre-v1.112 requests, traits are not in the schema so they won\u0027t"},{"line_number":457,"context_line":"        # be present. For v1.112+ they may be provided."},{"line_number":458,"context_line":"        if not api_utils.allow_runbook_traits():"},{"line_number":459,"context_line":"            runbook.pop(\u0027traits\u0027, None)"},{"line_number":460,"context_line":""},{"line_number":461,"context_line":"        # Ensure traits is always set so the notification payload can be"},{"line_number":462,"context_line":"        # populated (RunbookCRUDPayload.SCHEMA includes \u0027traits\u0027)."}],"source_content_type":"text/x-python","patch_set":6,"id":"e1875add_3fabddd5","line":459,"in_reply_to":"ef41f0c6_a3f05fdf","updated":"2026-04-08 18:28:04.000000000","message":"Done","commit_id":"b0f09795c0b1c3aa76385f4fac8545b2dab4ca25"},{"author":{"_account_id":10342,"name":"Jay Faulkner","display_name":"JayF","email":"jay@jvf.cc","username":"JayF","status":"youtube.com/@oss-gr / podcast.gr-oss.io"},"change_message_id":"c9a28552722cad9be7361bed75d240a899083a8a","unresolved":true,"context_lines":[{"line_number":43,"context_line":"# JSON schema for the runbook name when API version \u003e\u003d 1.112."},{"line_number":44,"context_line":"# Names must still be valid RFC 3986 unreserved-character strings so that they"},{"line_number":45,"context_line":"# can be resolved by name in URL paths (ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\")."},{"line_number":46,"context_line":"RUNBOOK_NAME_SCHEMA \u003d {"},{"line_number":47,"context_line":"    \u0027type\u0027: \u0027string\u0027,"},{"line_number":48,"context_line":"    \u0027minLength\u0027: 1,"},{"line_number":49,"context_line":"    \u0027maxLength\u0027: 255,"}],"source_content_type":"text/x-python","patch_set":7,"id":"12d6bc21_17b86767","line":46,"updated":"2026-04-08 20:48:31.000000000","message":"nit: ideally we would transfer this endpoint over to the new schema method now that we\u0027re touching it","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"411b19a20d545c9b474164d86a0edd7e109a0c8a","unresolved":true,"context_lines":[{"line_number":43,"context_line":"# JSON schema for the runbook name when API version \u003e\u003d 1.112."},{"line_number":44,"context_line":"# Names must still be valid RFC 3986 unreserved-character strings so that they"},{"line_number":45,"context_line":"# can be resolved by name in URL paths (ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\")."},{"line_number":46,"context_line":"RUNBOOK_NAME_SCHEMA \u003d {"},{"line_number":47,"context_line":"    \u0027type\u0027: \u0027string\u0027,"},{"line_number":48,"context_line":"    \u0027minLength\u0027: 1,"},{"line_number":49,"context_line":"    \u0027maxLength\u0027: 255,"}],"source_content_type":"text/x-python","patch_set":7,"id":"9b95e372_4127439b","line":46,"in_reply_to":"12d6bc21_17b86767","updated":"2026-04-08 21:38:19.000000000","message":"In a follow up to keep this smaller.","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":10342,"name":"Jay Faulkner","display_name":"JayF","email":"jay@jvf.cc","username":"JayF","status":"youtube.com/@oss-gr / podcast.gr-oss.io"},"change_message_id":"c9a28552722cad9be7361bed75d240a899083a8a","unresolved":true,"context_lines":[{"line_number":258,"context_line":"                raise exception.Invalid(msg)"},{"line_number":259,"context_line":"            new_traits \u003d set(rpc_runbook.traits) | {trait}"},{"line_number":260,"context_line":"            objects.RunbookTraitList.create("},{"line_number":261,"context_line":"                context, rpc_runbook.id, sorted(new_traits))"},{"line_number":262,"context_line":"            # Set the HTTP Location Header"},{"line_number":263,"context_line":"            url_args \u003d \u0027/\u0027.join((self.runbook_ident, \u0027traits\u0027, trait))"},{"line_number":264,"context_line":"            api.response.location \u003d link.build_url(\u0027runbooks\u0027, url_args)"}],"source_content_type":"text/x-python","patch_set":7,"id":"fb1d0168_d24c73bc","line":261,"updated":"2026-04-08 20:48:31.000000000","message":"Found a race here. Worked through it with claude for a description:\n\n  So two concurrent PUT /v1/runbooks/{ident}/traits/b and .../traits/c against an existing {a} can race:                       \n  - T1 reads {a}, T2 reads {a}\n  - T1 writes {a,b}, T2 writes {a,c} → final {a,c}, b is lost.                                                                 \n                                                                                                                             \n  The node-traits equivalent avoids this entirely by routing the single-trait PUT through add_node_trait (a single INSERT,     \n  idempotent on PK conflict) rather than read-modify-write of the full set. That\u0027s the fix I\u0027d recommend, and it\u0027s also why I  \n  flagged it as semantically diverging from the node-traits pattern, not just racy.","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"411b19a20d545c9b474164d86a0edd7e109a0c8a","unresolved":false,"context_lines":[{"line_number":258,"context_line":"                raise exception.Invalid(msg)"},{"line_number":259,"context_line":"            new_traits \u003d set(rpc_runbook.traits) | {trait}"},{"line_number":260,"context_line":"            objects.RunbookTraitList.create("},{"line_number":261,"context_line":"                context, rpc_runbook.id, sorted(new_traits))"},{"line_number":262,"context_line":"            # Set the HTTP Location Header"},{"line_number":263,"context_line":"            url_args \u003d \u0027/\u0027.join((self.runbook_ident, \u0027traits\u0027, trait))"},{"line_number":264,"context_line":"            api.response.location \u003d link.build_url(\u0027runbooks\u0027, url_args)"}],"source_content_type":"text/x-python","patch_set":7,"id":"5af771e3_1dbcd6ce","line":261,"in_reply_to":"fb1d0168_d24c73bc","updated":"2026-04-08 21:38:19.000000000","message":"Done","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":10342,"name":"Jay Faulkner","display_name":"JayF","email":"jay@jvf.cc","username":"JayF","status":"youtube.com/@oss-gr / podcast.gr-oss.io"},"change_message_id":"c9a28552722cad9be7361bed75d240a899083a8a","unresolved":true,"context_lines":[{"line_number":300,"context_line":"            if not api_utils.allow_runbook_traits():"},{"line_number":301,"context_line":"                msg \u003d _(\"The API version does not allow runbook traits\")"},{"line_number":302,"context_line":"                raise webob_exc.HTTPNotFound(msg)"},{"line_number":303,"context_line":"            return RunbookTraitsController(runbook_ident), remainder[1:]"},{"line_number":304,"context_line":""},{"line_number":305,"context_line":"    @pecan.expose()"},{"line_number":306,"context_line":"    def _route(self, args, request\u003dNone):"}],"source_content_type":"text/x-python","patch_set":7,"id":"4916ad41_00fe3828","line":303,"updated":"2026-04-08 20:48:31.000000000","message":"the else path here needs to return or raise as appropriate as it does in other controllers","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"4b0663287fb017e62790b2e68796e5a894458dc3","unresolved":false,"context_lines":[{"line_number":300,"context_line":"            if not api_utils.allow_runbook_traits():"},{"line_number":301,"context_line":"                msg \u003d _(\"The API version does not allow runbook traits\")"},{"line_number":302,"context_line":"                raise webob_exc.HTTPNotFound(msg)"},{"line_number":303,"context_line":"            return RunbookTraitsController(runbook_ident), remainder[1:]"},{"line_number":304,"context_line":""},{"line_number":305,"context_line":"    @pecan.expose()"},{"line_number":306,"context_line":"    def _route(self, args, request\u003dNone):"}],"source_content_type":"text/x-python","patch_set":7,"id":"48c4e2c9_9e4fe5c6","line":303,"in_reply_to":"256bdb86_42e5d41f","updated":"2026-04-10 05:02:11.000000000","message":"Acknowledged","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"411b19a20d545c9b474164d86a0edd7e109a0c8a","unresolved":true,"context_lines":[{"line_number":300,"context_line":"            if not api_utils.allow_runbook_traits():"},{"line_number":301,"context_line":"                msg \u003d _(\"The API version does not allow runbook traits\")"},{"line_number":302,"context_line":"                raise webob_exc.HTTPNotFound(msg)"},{"line_number":303,"context_line":"            return RunbookTraitsController(runbook_ident), remainder[1:]"},{"line_number":304,"context_line":""},{"line_number":305,"context_line":"    @pecan.expose()"},{"line_number":306,"context_line":"    def _route(self, args, request\u003dNone):"}],"source_content_type":"text/x-python","patch_set":7,"id":"caef8f4e_a5ea4e05","line":303,"in_reply_to":"4916ad41_00fe3828","updated":"2026-04-08 21:38:19.000000000","message":"Can you give me an example? I\u0027m seeing most of them fall through. I\u0027ve looked at node, event, and portgroup.","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":10342,"name":"Jay Faulkner","display_name":"JayF","email":"jay@jvf.cc","username":"JayF","status":"youtube.com/@oss-gr / podcast.gr-oss.io"},"change_message_id":"70a1e4484e91d7f65a9c17ab6d6515c472cb51c7","unresolved":true,"context_lines":[{"line_number":300,"context_line":"            if not api_utils.allow_runbook_traits():"},{"line_number":301,"context_line":"                msg \u003d _(\"The API version does not allow runbook traits\")"},{"line_number":302,"context_line":"                raise webob_exc.HTTPNotFound(msg)"},{"line_number":303,"context_line":"            return RunbookTraitsController(runbook_ident), remainder[1:]"},{"line_number":304,"context_line":""},{"line_number":305,"context_line":"    @pecan.expose()"},{"line_number":306,"context_line":"    def _route(self, args, request\u003dNone):"}],"source_content_type":"text/x-python","patch_set":7,"id":"256bdb86_42e5d41f","line":303,"in_reply_to":"c827ba4d_a8fbe104","updated":"2026-04-09 15:41:19.000000000","message":"As we talked about with words, it seems like there\u0027s an inconsistent use across our codebase and what\u0027s here works.","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"e403ffe091938724607588a37e236a685a9ffb46","unresolved":true,"context_lines":[{"line_number":300,"context_line":"            if not api_utils.allow_runbook_traits():"},{"line_number":301,"context_line":"                msg \u003d _(\"The API version does not allow runbook traits\")"},{"line_number":302,"context_line":"                raise webob_exc.HTTPNotFound(msg)"},{"line_number":303,"context_line":"            return RunbookTraitsController(runbook_ident), remainder[1:]"},{"line_number":304,"context_line":""},{"line_number":305,"context_line":"    @pecan.expose()"},{"line_number":306,"context_line":"    def _route(self, args, request\u003dNone):"}],"source_content_type":"text/x-python","patch_set":7,"id":"c827ba4d_a8fbe104","line":303,"in_reply_to":"caef8f4e_a5ea4e05","updated":"2026-04-09 00:44:41.000000000","message":"This actually breaks the tempest tests and requires me to change a number of other tests in the code base because the fall through case causes pecan to return a 405 which is what\u0027s checked in tempest and other tests.","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"05452e924c04818aeb4742d6f4e8265dae5d69d7","unresolved":true,"context_lines":[{"line_number":80,"context_line":"}"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"# Schema used when validating PATCH bodies for v1.112+.  Traits are not"},{"line_number":83,"context_line":"# patchable via this endpoint (use the /traits sub-resource instead), so"},{"line_number":84,"context_line":"# \u0027traits\u0027 is intentionally absent from the properties here."},{"line_number":85,"context_line":"RUNBOOK_PATCH_SCHEMA_V112 \u003d {"},{"line_number":86,"context_line":"    \u0027type\u0027: \u0027object\u0027,"}],"source_content_type":"text/x-python","patch_set":10,"id":"4f174f15_3d6ac85a","line":83,"updated":"2026-04-09 17:35:27.000000000","message":"It\u0027s going to be pretty mind-twisting that traits can be included in the body on creation but not patched. I don\u0027t think node traits have this problem since they cannot be specified on creation (IIRC). Maybe it\u0027s less confusing to allow patch for traits too?","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"1abc98eff08a630aa9c1de99ff3e87590eaf8de2","unresolved":true,"context_lines":[{"line_number":80,"context_line":"}"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"# Schema used when validating PATCH bodies for v1.112+.  Traits are not"},{"line_number":83,"context_line":"# patchable via this endpoint (use the /traits sub-resource instead), so"},{"line_number":84,"context_line":"# \u0027traits\u0027 is intentionally absent from the properties here."},{"line_number":85,"context_line":"RUNBOOK_PATCH_SCHEMA_V112 \u003d {"},{"line_number":86,"context_line":"    \u0027type\u0027: \u0027object\u0027,"}],"source_content_type":"text/x-python","patch_set":10,"id":"dfd96636_75e6fe9a","line":83,"in_reply_to":"266c9af3_9f8aaac5","updated":"2026-04-10 12:39:57.000000000","message":"Sigh, the price we\u0027re paying for not having e-tags... But doesn\u0027t the same situation happen with steps?","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"ca114f8a694e7050d9786ab57b9eb5d384fc50e3","unresolved":true,"context_lines":[{"line_number":80,"context_line":"}"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"# Schema used when validating PATCH bodies for v1.112+.  Traits are not"},{"line_number":83,"context_line":"# patchable via this endpoint (use the /traits sub-resource instead), so"},{"line_number":84,"context_line":"# \u0027traits\u0027 is intentionally absent from the properties here."},{"line_number":85,"context_line":"RUNBOOK_PATCH_SCHEMA_V112 \u003d {"},{"line_number":86,"context_line":"    \u0027type\u0027: \u0027object\u0027,"}],"source_content_type":"text/x-python","patch_set":10,"id":"266c9af3_9f8aaac5","line":83,"in_reply_to":"4f174f15_3d6ac85a","updated":"2026-04-10 03:36:47.000000000","message":"So trait CUSTOM_FOO and CUSTOM_BAR are on the runbook. User A reads the current traits and starts editing the list down to CUSTOM_FOO and while they are making their list user B adds CUSTOM_BAZ as a trait on the runbook. Now user A does a PATCH with their list. User B queries the traits on the runbook and sees only CUSTOM_FOO and files an Ironic bug. That’s why I didn’t include it.","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"bbd718879d6b725b357323718afb6c040fb6d526","unresolved":true,"context_lines":[{"line_number":80,"context_line":"}"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"# Schema used when validating PATCH bodies for v1.112+.  Traits are not"},{"line_number":83,"context_line":"# patchable via this endpoint (use the /traits sub-resource instead), so"},{"line_number":84,"context_line":"# \u0027traits\u0027 is intentionally absent from the properties here."},{"line_number":85,"context_line":"RUNBOOK_PATCH_SCHEMA_V112 \u003d {"},{"line_number":86,"context_line":"    \u0027type\u0027: \u0027object\u0027,"}],"source_content_type":"text/x-python","patch_set":10,"id":"34c849a4_020d111e","line":83,"in_reply_to":"dfd96636_75e6fe9a","updated":"2026-04-10 13:49:15.000000000","message":"Yes but steps have always been a client side edit. While traits in my example would be a call of \"append CUSTOM_BAZ\" and they get a 200 and then its not there. The steps edit is a \"we\u0027ve wholesale replaced your steps\" and there\u0027s an updated at which can be checked with that. I\u0027m just rationalizing the explanation that Jay gave me.","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"05452e924c04818aeb4742d6f4e8265dae5d69d7","unresolved":true,"context_lines":[{"line_number":82,"context_line":"# Schema used when validating PATCH bodies for v1.112+.  Traits are not"},{"line_number":83,"context_line":"# patchable via this endpoint (use the /traits sub-resource instead), so"},{"line_number":84,"context_line":"# \u0027traits\u0027 is intentionally absent from the properties here."},{"line_number":85,"context_line":"RUNBOOK_PATCH_SCHEMA_V112 \u003d {"},{"line_number":86,"context_line":"    \u0027type\u0027: \u0027object\u0027,"},{"line_number":87,"context_line":"    \u0027properties\u0027: {"},{"line_number":88,"context_line":"        \u0027uuid\u0027: {\u0027type\u0027: [\u0027string\u0027, \u0027null\u0027]},"}],"source_content_type":"text/x-python","patch_set":10,"id":"94b6d6f8_97c19f7a","line":85,"updated":"2026-04-09 17:35:27.000000000","message":"nit: isn\u0027t it just a copy of RUNBOOK_SCHEMA_V112 with \u0027traits\u0027 pop()\u0027ed?","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"ca114f8a694e7050d9786ab57b9eb5d384fc50e3","unresolved":false,"context_lines":[{"line_number":82,"context_line":"# Schema used when validating PATCH bodies for v1.112+.  Traits are not"},{"line_number":83,"context_line":"# patchable via this endpoint (use the /traits sub-resource instead), so"},{"line_number":84,"context_line":"# \u0027traits\u0027 is intentionally absent from the properties here."},{"line_number":85,"context_line":"RUNBOOK_PATCH_SCHEMA_V112 \u003d {"},{"line_number":86,"context_line":"    \u0027type\u0027: \u0027object\u0027,"},{"line_number":87,"context_line":"    \u0027properties\u0027: {"},{"line_number":88,"context_line":"        \u0027uuid\u0027: {\u0027type\u0027: [\u0027string\u0027, \u0027null\u0027]},"}],"source_content_type":"text/x-python","patch_set":10,"id":"acd1dba4_6e908bc4","line":85,"in_reply_to":"94b6d6f8_97c19f7a","updated":"2026-04-10 03:36:47.000000000","message":"Done","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"}],"ironic/conductor/manager.py":[{"author":{"_account_id":4571,"name":"Steve Baker","email":"sbaker@redhat.com","username":"steve-stevebaker"},"change_message_id":"f92b64182c6c2ef44e6d90d7832457d52754538b","unresolved":true,"context_lines":[{"line_number":1248,"context_line":"                        LOG.error(msg)"},{"line_number":1249,"context_line":"                        raise exception.NodeCleaningFailure("},{"line_number":1250,"context_line":"                            node\u003dnode, message\u003dmsg)"},{"line_number":1251,"context_line":"                else:"},{"line_number":1252,"context_line":"                    # Legacy style: runbook name must match a node trait."},{"line_number":1253,"context_line":"                    if runbook.name not in node_trait_names:"},{"line_number":1254,"context_line":"                        msg \u003d _("}],"source_content_type":"text/x-python","patch_set":11,"id":"181e3c12_3d17da73","line":1251,"updated":"2026-04-13 02:32:33.000000000","message":"Is the legacy path even necessary with the database migration? There should probably be a distinction between the runbook having a traits attribute vs the traits attribute being an empty list. This legacy block only needs to run when:\n\n    elif not hasattr(runbook, \u0027traits\u0027):","commit_id":"fc74fb01ec94f25d2d5df6e10c8e39b370e5f6fd"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"0c9ae473104d8687c982787d27e627327fee1bbb","unresolved":true,"context_lines":[{"line_number":1248,"context_line":"                        LOG.error(msg)"},{"line_number":1249,"context_line":"                        raise exception.NodeCleaningFailure("},{"line_number":1250,"context_line":"                            node\u003dnode, message\u003dmsg)"},{"line_number":1251,"context_line":"                else:"},{"line_number":1252,"context_line":"                    # Legacy style: runbook name must match a node trait."},{"line_number":1253,"context_line":"                    if runbook.name not in node_trait_names:"},{"line_number":1254,"context_line":"                        msg \u003d _("}],"source_content_type":"text/x-python","patch_set":11,"id":"82ad22c1_243dd4f8","line":1251,"in_reply_to":"181e3c12_3d17da73","updated":"2026-04-13 14:37:38.000000000","message":"It\u0027s to support the case that someone hasn\u0027t run the online database migration before a cleaning has begun. Cause otherwise that box won\u0027t be cleaned correctly.","commit_id":"fc74fb01ec94f25d2d5df6e10c8e39b370e5f6fd"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"84be8b1ca7c8651f71a37c436fa01dbd21597918","unresolved":true,"context_lines":[{"line_number":1248,"context_line":"                        LOG.error(msg)"},{"line_number":1249,"context_line":"                        raise exception.NodeCleaningFailure("},{"line_number":1250,"context_line":"                            node\u003dnode, message\u003dmsg)"},{"line_number":1251,"context_line":"                else:"},{"line_number":1252,"context_line":"                    # Legacy style: runbook name must match a node trait."},{"line_number":1253,"context_line":"                    if runbook.name not in node_trait_names:"},{"line_number":1254,"context_line":"                        msg \u003d _("}],"source_content_type":"text/x-python","patch_set":11,"id":"ab74db63_37231a00","line":1251,"in_reply_to":"82ad22c1_243dd4f8","updated":"2026-04-13 15:34:22.000000000","message":"Ah no. Now walking through it. Updating.","commit_id":"fc74fb01ec94f25d2d5df6e10c8e39b370e5f6fd"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"9e154bcb900252199a4ab25ea17102350b24de91","unresolved":false,"context_lines":[{"line_number":1248,"context_line":"                        LOG.error(msg)"},{"line_number":1249,"context_line":"                        raise exception.NodeCleaningFailure("},{"line_number":1250,"context_line":"                            node\u003dnode, message\u003dmsg)"},{"line_number":1251,"context_line":"                else:"},{"line_number":1252,"context_line":"                    # Legacy style: runbook name must match a node trait."},{"line_number":1253,"context_line":"                    if runbook.name not in node_trait_names:"},{"line_number":1254,"context_line":"                        msg \u003d _("}],"source_content_type":"text/x-python","patch_set":11,"id":"91d54dda_abd3994f","line":1251,"in_reply_to":"ab74db63_37231a00","updated":"2026-04-13 15:34:51.000000000","message":"Done","commit_id":"fc74fb01ec94f25d2d5df6e10c8e39b370e5f6fd"}],"ironic/db/sqlalchemy/alembic/versions/e2d316b60d9e_add_runbook_traits_and_description.py":[{"author":{"_account_id":11655,"name":"Julia Kreger","email":"juliaashleykreger@gmail.com","username":"jkreger","status":"Flying to the moon with a Jetpack!"},"change_message_id":"8bad3f79fb197877467ed96ca5b92db9822faf88","unresolved":true,"context_lines":[{"line_number":49,"context_line":"        mysql_charset\u003d\u0027utf8mb4\u0027"},{"line_number":50,"context_line":"    )"},{"line_number":51,"context_line":""},{"line_number":52,"context_line":"    # Migrate existing runbook names into runbook_traits."},{"line_number":53,"context_line":"    # Each existing runbook had its name constrained to a valid trait string,"},{"line_number":54,"context_line":"    # so we seed the traits set with the current name to preserve behaviour."},{"line_number":55,"context_line":"    bind \u003d op.get_bind()"}],"source_content_type":"text/x-python","patch_set":3,"id":"101862b8_9f965b62","line":52,"updated":"2026-04-06 20:19:03.000000000","message":"So, I think this is the wrong approach. I think we should do this in the online_data_migration code model, since we\u0027re really not supposed to be doing data migrations (locking, and all) and upgrades are often delayed a little, so doing it in online data migrations allows for the schema to be in place from this change running, and then as someone is upgrading their deployment, they can run online data migrations to ensure the data is in sync.\n\nSpecifically: The online data migrations should pull the list of records, like is done below, and then make sure there are no additional records to be migrated, in such case then the online data migration returns success., if not, then it just needs to migrate records in a chunked fashion.","commit_id":"8529de71eb9eef021b80c2cf7023e463ac3e8225"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"bc1bf0ad1e64bf4e0b877ba374129431b2f2b155","unresolved":false,"context_lines":[{"line_number":49,"context_line":"        mysql_charset\u003d\u0027utf8mb4\u0027"},{"line_number":50,"context_line":"    )"},{"line_number":51,"context_line":""},{"line_number":52,"context_line":"    # Migrate existing runbook names into runbook_traits."},{"line_number":53,"context_line":"    # Each existing runbook had its name constrained to a valid trait string,"},{"line_number":54,"context_line":"    # so we seed the traits set with the current name to preserve behaviour."},{"line_number":55,"context_line":"    bind \u003d op.get_bind()"}],"source_content_type":"text/x-python","patch_set":3,"id":"14b48922_eb22dfb4","line":52,"in_reply_to":"101862b8_9f965b62","updated":"2026-04-08 15:18:00.000000000","message":"Done","commit_id":"8529de71eb9eef021b80c2cf7023e463ac3e8225"},{"author":{"_account_id":11655,"name":"Julia Kreger","email":"juliaashleykreger@gmail.com","username":"jkreger","status":"Flying to the moon with a Jetpack!"},"change_message_id":"8bad3f79fb197877467ed96ca5b92db9822faf88","unresolved":false,"context_lines":[{"line_number":63,"context_line":"        session.execute("},{"line_number":64,"context_line":"            sa.text("},{"line_number":65,"context_line":"                \u0027INSERT INTO runbook_traits (runbook_id, trait) \u0027"},{"line_number":66,"context_line":"                \u0027VALUES (:runbook_id, :trait)\u0027"},{"line_number":67,"context_line":"            ),"},{"line_number":68,"context_line":"            [{\u0027runbook_id\u0027: row[0], \u0027trait\u0027: row[1]} for row in runbooks]"},{"line_number":69,"context_line":"        )"}],"source_content_type":"text/x-python","patch_set":3,"id":"3c8aa7dc_7acc03fe","line":66,"updated":"2026-04-06 20:19:03.000000000","message":"cool, bulk insert!","commit_id":"8529de71eb9eef021b80c2cf7023e463ac3e8225"},{"author":{"_account_id":37208,"name":"Marek Skrobacki","display_name":"Marek Skrobacki","email":"skrobul@skrobul.com","username":"skrobul"},"change_message_id":"37ec9307a1f3e018a9ed893fe2d81406655467b8","unresolved":true,"context_lines":[{"line_number":13,"context_line":"\"\"\"Add runbook_traits table and description column to runbooks"},{"line_number":14,"context_line":""},{"line_number":15,"context_line":"Revision ID: e2d316b60d9e"},{"line_number":16,"context_line":"Revises: c1fd28861bb9"},{"line_number":17,"context_line":"Create Date: 2026-03-26 00:00:00.000000"},{"line_number":18,"context_line":""},{"line_number":19,"context_line":"\"\"\""}],"source_content_type":"text/x-python","patch_set":4,"id":"14dd7749_d17fad62","line":16,"updated":"2026-04-08 15:28:33.000000000","message":"the down_revision is 2a3b4c5d6e7f - do the have to match(i cannot remember)?","commit_id":"36c6b9b5ed53dadc5cbcb416b8499c1ca853d386"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"da468628f5cae7baf19120900bbb39810dd1da0d","unresolved":true,"context_lines":[{"line_number":13,"context_line":"\"\"\"Add runbook_traits table and description column to runbooks"},{"line_number":14,"context_line":""},{"line_number":15,"context_line":"Revision ID: e2d316b60d9e"},{"line_number":16,"context_line":"Revises: c1fd28861bb9"},{"line_number":17,"context_line":"Create Date: 2026-03-26 00:00:00.000000"},{"line_number":18,"context_line":""},{"line_number":19,"context_line":"\"\"\""}],"source_content_type":"text/x-python","patch_set":4,"id":"e660a87b_e3e15e42","line":16,"in_reply_to":"14dd7749_d17fad62","updated":"2026-04-08 16:47:54.000000000","message":"Yes. Good catch. This was wrong.","commit_id":"36c6b9b5ed53dadc5cbcb416b8499c1ca853d386"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"393a82a26bd68bb828aae2443a5083bf11bdf1d1","unresolved":false,"context_lines":[{"line_number":13,"context_line":"\"\"\"Add runbook_traits table and description column to runbooks"},{"line_number":14,"context_line":""},{"line_number":15,"context_line":"Revision ID: e2d316b60d9e"},{"line_number":16,"context_line":"Revises: c1fd28861bb9"},{"line_number":17,"context_line":"Create Date: 2026-03-26 00:00:00.000000"},{"line_number":18,"context_line":""},{"line_number":19,"context_line":"\"\"\""}],"source_content_type":"text/x-python","patch_set":4,"id":"d0a94904_d68c421b","line":16,"in_reply_to":"e660a87b_e3e15e42","updated":"2026-04-08 16:52:51.000000000","message":"Done","commit_id":"36c6b9b5ed53dadc5cbcb416b8499c1ca853d386"}],"ironic/db/sqlalchemy/api.py":[{"author":{"_account_id":11655,"name":"Julia Kreger","email":"juliaashleykreger@gmail.com","username":"jkreger","status":"Flying to the moon with a Jetpack!"},"change_message_id":"1d1bf90aa231a56acdb507582da35a349234f655","unresolved":true,"context_lines":[{"line_number":2210,"context_line":"                     .outerjoin(models.RunbookTrait,"},{"line_number":2211,"context_line":"                                models.Runbook.id"},{"line_number":2212,"context_line":"                                \u003d\u003d models.RunbookTrait.runbook_id)"},{"line_number":2213,"context_line":"                     .filter(models.RunbookTrait.runbook_id.is_(None)))"},{"line_number":2214,"context_line":""},{"line_number":2215,"context_line":"            total_count \u003d query.count()"},{"line_number":2216,"context_line":"            if not total_count:"}],"source_content_type":"text/x-python","patch_set":4,"id":"4b41d31c_a80ba90b","line":2213,"updated":"2026-04-08 15:06:20.000000000","message":"Technically speaking, you could re-use the query by starting the session for write out of the box instead of re-running, but its likely not a big deal.","commit_id":"36c6b9b5ed53dadc5cbcb416b8499c1ca853d386"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"393a82a26bd68bb828aae2443a5083bf11bdf1d1","unresolved":false,"context_lines":[{"line_number":2210,"context_line":"                     .outerjoin(models.RunbookTrait,"},{"line_number":2211,"context_line":"                                models.Runbook.id"},{"line_number":2212,"context_line":"                                \u003d\u003d models.RunbookTrait.runbook_id)"},{"line_number":2213,"context_line":"                     .filter(models.RunbookTrait.runbook_id.is_(None)))"},{"line_number":2214,"context_line":""},{"line_number":2215,"context_line":"            total_count \u003d query.count()"},{"line_number":2216,"context_line":"            if not total_count:"}],"source_content_type":"text/x-python","patch_set":4,"id":"6bc632cd_5956b5bb","line":2213,"in_reply_to":"4b41d31c_a80ba90b","updated":"2026-04-08 16:52:51.000000000","message":"Done","commit_id":"36c6b9b5ed53dadc5cbcb416b8499c1ca853d386"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"05452e924c04818aeb4742d6f4e8265dae5d69d7","unresolved":true,"context_lines":[{"line_number":2219,"context_line":"                session.add(trait)"},{"line_number":2220,"context_line":"                num_migrated +\u003d 1"},{"line_number":2221,"context_line":""},{"line_number":2222,"context_line":"            session.flush()"},{"line_number":2223,"context_line":""},{"line_number":2224,"context_line":"        return total_to_migrate, num_migrated"},{"line_number":2225,"context_line":""}],"source_content_type":"text/x-python","patch_set":10,"id":"3cec8625_5b35c5fb","line":2222,"updated":"2026-04-09 17:35:27.000000000","message":"nit: should not be needed (unless you intended to do it inside of the for loop)","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"05452e924c04818aeb4742d6f4e8265dae5d69d7","unresolved":true,"context_lines":[{"line_number":2831,"context_line":"                    runbook_trait \u003d models.RunbookTrait("},{"line_number":2832,"context_line":"                        trait\u003dtrait, runbook_id\u003drunbook.id, version\u003d\u00271.0\u0027)"},{"line_number":2833,"context_line":"                    session.add(runbook_trait)"},{"line_number":2834,"context_line":"                session.flush()"},{"line_number":2835,"context_line":"            except db_exc.DBDuplicateEntry as e:"},{"line_number":2836,"context_line":"                if \u0027name\u0027 in e.columns:"},{"line_number":2837,"context_line":"                    raise exception.RunbookDuplicateName("}],"source_content_type":"text/x-python","patch_set":10,"id":"de688572_dba6c8de","line":2834,"updated":"2026-04-09 17:35:27.000000000","message":"nit: flash not needed (used above to get RunbookAlreadyExists at the expected place)","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":11655,"name":"Julia Kreger","email":"juliaashleykreger@gmail.com","username":"jkreger","status":"Flying to the moon with a Jetpack!"},"change_message_id":"f5594ce0a0c3b90c4a47fe19359cface494f3048","unresolved":true,"context_lines":[{"line_number":2825,"context_line":"        with _session_for_write() as session:"},{"line_number":2826,"context_line":"            try:"},{"line_number":2827,"context_line":"                session.add(runbook)"},{"line_number":2828,"context_line":"                session.flush()"},{"line_number":2829,"context_line":"                # Now add traits using the flushed runbook id"},{"line_number":2830,"context_line":"                for trait in unique_traits:"},{"line_number":2831,"context_line":"                    runbook_trait \u003d models.RunbookTrait("}],"source_content_type":"text/x-python","patch_set":12,"id":"b8c8aab3_9794a519","line":2828,"range":{"start_line":2828,"start_character":16,"end_line":2828,"end_character":31},"updated":"2026-04-13 19:17:35.000000000","message":"nit: Do we really need this flush if we have a flush?","commit_id":"88ae5713654e7829f751f9b7c2ea895b5d505fd6"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"7e1090aea8824126831122140ce444fb3f9b16c5","unresolved":true,"context_lines":[{"line_number":2825,"context_line":"        with _session_for_write() as session:"},{"line_number":2826,"context_line":"            try:"},{"line_number":2827,"context_line":"                session.add(runbook)"},{"line_number":2828,"context_line":"                session.flush()"},{"line_number":2829,"context_line":"                # Now add traits using the flushed runbook id"},{"line_number":2830,"context_line":"                for trait in unique_traits:"},{"line_number":2831,"context_line":"                    runbook_trait \u003d models.RunbookTrait("}],"source_content_type":"text/x-python","patch_set":12,"id":"7d2f3db3_7dae303f","line":2828,"range":{"start_line":2828,"start_character":16,"end_line":2828,"end_character":31},"in_reply_to":"b8c8aab3_9794a519","updated":"2026-04-13 19:39:12.000000000","message":"Yeah I think this flush is what we want and the one below is what we don\u0027t need. Can I play with the flushes in a follow up?","commit_id":"88ae5713654e7829f751f9b7c2ea895b5d505fd6"}],"ironic/tests/unit/api/controllers/v1/test_runbook.py":[{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"05452e924c04818aeb4742d6f4e8265dae5d69d7","unresolved":true,"context_lines":[{"line_number":1300,"context_line":"        self.assertEqual(http_client.NOT_FOUND, response.status_int)"},{"line_number":1301,"context_line":""},{"line_number":1302,"context_line":""},{"line_number":1303,"context_line":"class TestRunbookV112(BaseRunbooksAPITest):"},{"line_number":1304,"context_line":"    \"\"\"Tests for runbook features introduced in API v1.112.\"\"\""},{"line_number":1305,"context_line":""},{"line_number":1306,"context_line":"    # ------------------------------------------------- create (POST) --------"}],"source_content_type":"text/x-python","patch_set":10,"id":"ad68041d_7517c7cb","line":1303,"updated":"2026-04-09 17:35:27.000000000","message":"All tests in TestRunbookTraitsController also rely on 1.112, so I\u0027m not sure why the split.","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"ca114f8a694e7050d9786ab57b9eb5d384fc50e3","unresolved":false,"context_lines":[{"line_number":1300,"context_line":"        self.assertEqual(http_client.NOT_FOUND, response.status_int)"},{"line_number":1301,"context_line":""},{"line_number":1302,"context_line":""},{"line_number":1303,"context_line":"class TestRunbookV112(BaseRunbooksAPITest):"},{"line_number":1304,"context_line":"    \"\"\"Tests for runbook features introduced in API v1.112.\"\"\""},{"line_number":1305,"context_line":""},{"line_number":1306,"context_line":"    # ------------------------------------------------- create (POST) --------"}],"source_content_type":"text/x-python","patch_set":10,"id":"c8fc1b7b_b5097dd7","line":1303,"in_reply_to":"ad68041d_7517c7cb","updated":"2026-04-10 03:36:47.000000000","message":"Done","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"05452e924c04818aeb4742d6f4e8265dae5d69d7","unresolved":true,"context_lines":[{"line_number":1398,"context_line":"        self.assertIn(\u0027traits\u0027, response.json[\u0027error_message\u0027])"},{"line_number":1399,"context_line":""},{"line_number":1400,"context_line":"    # ---------------------------------------------------- patch (PATCH) ------"},{"line_number":1401,"context_line":""},{"line_number":1402,"context_line":"    @mock.patch.object(objects.Runbook, \u0027save\u0027, autospec\u003dTrue)"},{"line_number":1403,"context_line":"    def test_patch_description_v112(self, mock_save):"},{"line_number":1404,"context_line":"        runbook \u003d obj_utils.create_test_runbook("}],"source_content_type":"text/x-python","patch_set":10,"id":"85ce67e7_d18ab354","line":1401,"updated":"2026-04-09 17:35:27.000000000","message":"From the release note: it would be good to test that runbooks created with v1.112-style names are still readable and editable with older API versions.","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"ca114f8a694e7050d9786ab57b9eb5d384fc50e3","unresolved":false,"context_lines":[{"line_number":1398,"context_line":"        self.assertIn(\u0027traits\u0027, response.json[\u0027error_message\u0027])"},{"line_number":1399,"context_line":""},{"line_number":1400,"context_line":"    # ---------------------------------------------------- patch (PATCH) ------"},{"line_number":1401,"context_line":""},{"line_number":1402,"context_line":"    @mock.patch.object(objects.Runbook, \u0027save\u0027, autospec\u003dTrue)"},{"line_number":1403,"context_line":"    def test_patch_description_v112(self, mock_save):"},{"line_number":1404,"context_line":"        runbook \u003d obj_utils.create_test_runbook("}],"source_content_type":"text/x-python","patch_set":10,"id":"498b91a3_2022f25b","line":1401,"in_reply_to":"85ce67e7_d18ab354","updated":"2026-04-10 03:36:47.000000000","message":"Done","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"}],"ironic/tests/unit/conductor/test_manager.py":[{"author":{"_account_id":11655,"name":"Julia Kreger","email":"juliaashleykreger@gmail.com","username":"jkreger","status":"Flying to the moon with a Jetpack!"},"change_message_id":"8bad3f79fb197877467ed96ca5b92db9822faf88","unresolved":true,"context_lines":[{"line_number":7152,"context_line":"        rb.name \u003d bad_rb_name"},{"line_number":7153,"context_line":"#        exp_steps \u003d [{\u0027interface\u0027: \u0027deploy\u0027, \u0027step\u0027: \u0027test_step\u0027, \u0027args\u0027: {}}]"},{"line_number":7154,"context_line":"#        rb.steps \u003d exp_steps"},{"line_number":7155,"context_line":"#        rb.disable_ramdisk \u003d True"},{"line_number":7156,"context_line":"        mock_get_runbook.return_value \u003d rb"},{"line_number":7157,"context_line":""},{"line_number":7158,"context_line":"        self.assertRaises(exception.NodeCleaningFailure,"}],"source_content_type":"text/x-python","patch_set":3,"id":"31853a59_c2139ca3","side":"PARENT","line":7155,"updated":"2026-04-06 20:19:03.000000000","message":"*blink* *blink*","commit_id":"8c31ba09c6112af097621bc0bbd8dc30ca16c5b9"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"45f89864000bf0fc186eb7f6576ae5f35725021c","unresolved":true,"context_lines":[{"line_number":7152,"context_line":"        rb.name \u003d bad_rb_name"},{"line_number":7153,"context_line":"#        exp_steps \u003d [{\u0027interface\u0027: \u0027deploy\u0027, \u0027step\u0027: \u0027test_step\u0027, \u0027args\u0027: {}}]"},{"line_number":7154,"context_line":"#        rb.steps \u003d exp_steps"},{"line_number":7155,"context_line":"#        rb.disable_ramdisk \u003d True"},{"line_number":7156,"context_line":"        mock_get_runbook.return_value \u003d rb"},{"line_number":7157,"context_line":""},{"line_number":7158,"context_line":"        self.assertRaises(exception.NodeCleaningFailure,"}],"source_content_type":"text/x-python","patch_set":3,"id":"c5b5f648_a889fa04","side":"PARENT","line":7155,"in_reply_to":"31853a59_c2139ca3","updated":"2026-04-06 21:07:57.000000000","message":"yeah I dunno why this was here.","commit_id":"8c31ba09c6112af097621bc0bbd8dc30ca16c5b9"},{"author":{"_account_id":11655,"name":"Julia Kreger","email":"juliaashleykreger@gmail.com","username":"jkreger","status":"Flying to the moon with a Jetpack!"},"change_message_id":"242b737d417c2080efae56e1acf729b0ef0aa494","unresolved":false,"context_lines":[{"line_number":7152,"context_line":"        rb.name \u003d bad_rb_name"},{"line_number":7153,"context_line":"#        exp_steps \u003d [{\u0027interface\u0027: \u0027deploy\u0027, \u0027step\u0027: \u0027test_step\u0027, \u0027args\u0027: {}}]"},{"line_number":7154,"context_line":"#        rb.steps \u003d exp_steps"},{"line_number":7155,"context_line":"#        rb.disable_ramdisk \u003d True"},{"line_number":7156,"context_line":"        mock_get_runbook.return_value \u003d rb"},{"line_number":7157,"context_line":""},{"line_number":7158,"context_line":"        self.assertRaises(exception.NodeCleaningFailure,"}],"source_content_type":"text/x-python","patch_set":3,"id":"d505bc55_8abf6e12","side":"PARENT","line":7155,"in_reply_to":"c5b5f648_a889fa04","updated":"2026-04-08 13:45:38.000000000","message":"¯\\_(ツ)_/¯","commit_id":"8c31ba09c6112af097621bc0bbd8dc30ca16c5b9"}],"releasenotes/notes/runbook-traits-support-8f3a2b1c4d5e6f7a.yaml":[{"author":{"_account_id":10342,"name":"Jay Faulkner","display_name":"JayF","email":"jay@jvf.cc","username":"JayF","status":"youtube.com/@oss-gr / podcast.gr-oss.io"},"change_message_id":"c9a28552722cad9be7361bed75d240a899083a8a","unresolved":true,"context_lines":[],"source_content_type":"","patch_set":7,"id":"48da6f35_3fd617b2","line":36,"updated":"2026-04-08 20:48:31.000000000","message":"So there\u0027s a weird thing here, I think we should doc it instead of change it which is why this comment is on the release notes.\n\nI create a runbook under the new design.\n\nI then fetch it with an older microversion.\n\nWhereas before, I was guaranteed runbook.name would match CUSTOM_[A-Z][a-z].* (or something like that), now it can be nearly freeform.\n\nThe options we have are:\n- document the quirk (my vote)\n- hide runbooks which don\u0027t match (least favorite)\n- somehow munge the runbook names, in a reliable way -- maybe take the first trait or something? (nevermind I hate this too)","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"411b19a20d545c9b474164d86a0edd7e109a0c8a","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"b318bf43_cf8e2332","line":36,"in_reply_to":"48da6f35_3fd617b2","updated":"2026-04-08 21:38:19.000000000","message":"Done","commit_id":"65033e83b4fa6db3bda6ae092dc7a2ef695dbc9d"},{"author":{"_account_id":10342,"name":"Jay Faulkner","display_name":"JayF","email":"jay@jvf.cc","username":"JayF","status":"youtube.com/@oss-gr / podcast.gr-oss.io"},"change_message_id":"70a1e4484e91d7f65a9c17ab6d6515c472cb51c7","unresolved":true,"context_lines":[{"line_number":38,"context_line":"    runbook with a name that does not match the custom trait style or rename"},{"line_number":39,"context_line":"    one to not match and then use an older microversion API to query Ironic"},{"line_number":40,"context_line":"    the name will appear wrong and you will not be able to see the actual trait"},{"line_number":41,"context_line":"    that is matching your node."}],"source_content_type":"text/x-yaml","patch_set":10,"id":"7540b912_38cd8fa4","line":41,"updated":"2026-04-09 15:41:19.000000000","message":"I think we should be more specific here:\n\n\"Runbooks created using API microversion \u003e\u003d1.112, then later viewed with API microversions \u003c1.112, may have names that do not conform to the custom traits schema (CUSTOM_*) previously enforced for runbook names.\"","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":5890,"name":"Doug Goldstein","email":"cardoe@cardoe.com","username":"cardoe"},"change_message_id":"ca114f8a694e7050d9786ab57b9eb5d384fc50e3","unresolved":false,"context_lines":[{"line_number":38,"context_line":"    runbook with a name that does not match the custom trait style or rename"},{"line_number":39,"context_line":"    one to not match and then use an older microversion API to query Ironic"},{"line_number":40,"context_line":"    the name will appear wrong and you will not be able to see the actual trait"},{"line_number":41,"context_line":"    that is matching your node."}],"source_content_type":"text/x-yaml","patch_set":10,"id":"29f267fa_e25381ba","line":41,"in_reply_to":"469643c3_5be67d87","updated":"2026-04-10 03:36:47.000000000","message":"Done","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"},{"author":{"_account_id":10239,"name":"Dmitry Tantsur","email":"dtantsur@protonmail.com","username":"dtantsur"},"change_message_id":"05452e924c04818aeb4742d6f4e8265dae5d69d7","unresolved":true,"context_lines":[{"line_number":38,"context_line":"    runbook with a name that does not match the custom trait style or rename"},{"line_number":39,"context_line":"    one to not match and then use an older microversion API to query Ironic"},{"line_number":40,"context_line":"    the name will appear wrong and you will not be able to see the actual trait"},{"line_number":41,"context_line":"    that is matching your node."}],"source_content_type":"text/x-yaml","patch_set":10,"id":"469643c3_5be67d87","line":41,"in_reply_to":"7540b912_38cd8fa4","updated":"2026-04-09 17:35:27.000000000","message":"+1\n\nBtw, do we have a test that such runbooks can still be edited with an old API version?","commit_id":"72d4849455dd278c0a233cd4109a7e305052db1f"}]}
