Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cdk/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,17 @@ export default [
{
files: ['test/**/*.ts'],
rules: {
// Literal fixtures and expected values are the point of a test; naming every
// one defeats readability.
'@typescript-eslint/no-magic-numbers': 'off',
// Table-driven assertions and long expected strings read better on one line
// than wrapped.
'@stylistic/max-len': 'off',
'max-len': 'off',
// NOTE: `no-shadow` is deliberately NOT relaxed here. It is
// correctness-adjacent in test code — a shadowed `row` or `mock` inside a
// nested describe is a common way to assert against the wrong fixture and
// still pass. Rename the inner binding instead.
},
},
];
143 changes: 143 additions & 0 deletions cdk/src/constructs/orchestration-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* MIT No Attribution
*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

import { RemovalPolicy } from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';

/**
* Properties for OrchestrationTable construct.
*/
export interface OrchestrationTableProps {
/**
* Optional table name override.
* @default - auto-generated by CloudFormation
*/
readonly tableName?: string;

/**
* Removal policy for the table.
* @default RemovalPolicy.DESTROY
*/
readonly removalPolicy?: RemovalPolicy;

/**
* Whether to enable point-in-time recovery.
* @default true
*/
readonly pointInTimeRecovery?: boolean;
}

/**
* DynamoDB table holding the parent/sub-issue dependency graph (DAG)
* for sub-issue orchestration.
*
* One orchestration = one labeled Linear parent issue with sub-issues.
* Each child sub-issue is a row; the reconciler walks the rows to find
* children whose predecessors are all terminal-success and releases them
* via ``createTaskCore``.
*
* Schema: orchestration_id (PK), sub_issue_id (SK).
*
* Per-child row fields (written when the graph is discovered):
* - linear_sub_issue_id — the Linear sub-issue UUID this row tracks
* - child_task_id — the platform task_id created for this child (absent
* until the child is released by the reconciler)
* - depends_on — list of ``sub_issue_id``s that must reach
* terminal-success before this child may start
* - child_status — orchestration-local lifecycle marker (e.g.
* ``blocked`` | ``released`` | ``succeeded`` | ``failed`` | ``skipped``)
* - base_branch — the predecessor branch this child stacks on (ADR-001
* stacked PRs); ``main`` for root children
* - parent_linear_issue_id, linear_workspace_id, repo — provenance
*
* GSI:
* - ChildTaskIndex (PK: child_task_id) — the reconciler receives a
* child terminal-state event keyed by ``task_id`` and must resolve
* which orchestration + child row it belongs to. Sparse: only rows
* whose child has been released carry ``child_task_id``.
* - ChildBranchIndex (PK: child_branch_name) — the re-stack path receives
* a GitHub ``pull_request`` event keyed by head branch and must resolve
* which orchestration child opened that branch, so it can re-stack the
* child's dependents when its branch changes. Sparse: only released
* children carry ``child_branch_name``.
*
* NOTE: this construct is introduced but not yet instantiated in any
* stack — graph discovery and the reconciler wire it in. Synth-only for
* now keeps this foundational change deploy-safe.
*/
export class OrchestrationTable extends Construct {
/**
* GSI name for resolving a child ``task_id`` back to its
* orchestration + sub-issue row.
* PK: child_task_id. Sparse — only released children are projected.
*/
public static readonly CHILD_TASK_INDEX = 'ChildTaskIndex';

/**
* GSI name for resolving a child's head branch back to its
* orchestration + sub-issue row (used by the re-stack path).
* PK: child_branch_name. Sparse — only released children are projected.
*/
public static readonly CHILD_BRANCH_INDEX = 'ChildBranchIndex';

/**
* The underlying DynamoDB table. Use this to grant access or read the table name.
*/
public readonly table: dynamodb.Table;

constructor(scope: Construct, id: string, props: OrchestrationTableProps = {}) {
super(scope, id);

this.table = new dynamodb.Table(this, 'Table', {
tableName: props.tableName,
partitionKey: {
name: 'orchestration_id',
type: dynamodb.AttributeType.STRING,
},
sortKey: {
name: 'sub_issue_id',
type: dynamodb.AttributeType.STRING,
},
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
timeToLiveAttribute: 'ttl',
pointInTimeRecoverySpecification: {
pointInTimeRecoveryEnabled: props.pointInTimeRecovery ?? true,
},
removalPolicy: props.removalPolicy ?? RemovalPolicy.DESTROY,
});

// GSI: resolve a released child's task_id back to its orchestration row.
// Sparse — rows without child_task_id (not yet released) are not projected.
this.table.addGlobalSecondaryIndex({
indexName: OrchestrationTable.CHILD_TASK_INDEX,
partitionKey: { name: 'child_task_id', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});

// GSI: resolve a released child's head branch back to its orchestration
// row, so the re-stack path can find it. Sparse — rows without
// child_branch_name (not yet released) are not projected.
this.table.addGlobalSecondaryIndex({
indexName: OrchestrationTable.CHILD_BRANCH_INDEX,
partitionKey: { name: 'child_branch_name', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
}
}
Loading
Loading