Commit 2c6912a
docs(ci): document github/artifacts planfile runtime-token requirement + E2E test (#2649)
* docs(ci): document github/artifacts planfile runtime-token requirement
The github/artifacts planfile store talks to the GitHub Actions Artifacts
API directly and needs ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL, which
GitHub withholds from `run:` steps. That requirement was undocumented and
untested end-to-end.
- Add a "Using GitHub Artifacts in GitHub Actions" section to the planfile
storage docs, leading with the actions/github-runtime helper (mode: env),
and cross-reference it from the component docs.
- Generalize the in-repo actions/github-runtime action (action.yml + README)
so it advertises both `atmos ci cache` and `atmos terraform` (github/artifacts
planfile storage) — the JS action already re-exposes all ACTIONS_* generically.
- Add a GitHub Actions E2E workflow exercising the upload -> list -> download
round-trip against the real Artifacts API (the one path no Go unit test can
cover, since the runtime token only exists inside a runner).
- Align a cache-centric comment in the github artifact store.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ci): make planfile-artifacts E2E assert on download round-trip
The first run proved the upload works (the runtime creds surface via
github-runtime mode:env, and a fresh planfile artifact lands in the run),
but the assertion was wrong: `planfile list` renders artifacts without
opening each zip's metadata.json, so STACK/COMPONENT columns are empty and
an unfiltered list dumps every repo artifact — `grep mycomponent|prod`
could never match, and it blocked the real round-trip step from running.
Make `planfile download` (deterministic key from component+stack+SHA) the
gate, and demote `list` to informational. The harmless 403 WARNs from the
--ci check-run/PR-comment hooks are expected (we don't grant statuses/PR
write scope) and do not fail the plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ci): split planfile E2E into separate plan and apply jobs
Model the real plan-then-apply CI workflow across two jobs in one run:
- plan job: `atmos terraform plan --ci` uploads the planfile to GitHub
Actions Artifacts (creds surfaced via the github-runtime action, mode env).
- apply job (needs: plan): downloads that artifact cross-job via the named
"github" store and runs `atmos terraform apply --planfile=<downloaded>`,
applying exactly the reviewed plan with no re-plan.
Root-cause of the prior single-job failure: the --ci upload hook auto-detects
the runner and writes with prefix "planfile" to GITHUB_REPOSITORY (confirmed
via the artifacts API: planfile-prod--mycomponent--<sha>.tfplan.tar), ignoring
the named-store config. The dedicated fixture's "github" store now mirrors that
(prefix=planfile, no owner/repo) so the readback key matches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ci): download github/artifacts planfiles via runtime API for same-run reuse
The github/artifacts store downloaded only through the REST API
(GET /artifacts/{id}/zip), which GitHub does not serve while the producing
run is still in progress. That broke same-run plan-then-apply: a planfile
uploaded by a plan job could not be downloaded by a later apply job in the
same run ("zip: not a valid zip file").
Add a runtime-API download path mirroring the existing upload client: a
GetSignedArtifactURL Twirp call (scoped to the current run's backend IDs from
ACTIONS_RUNTIME_TOKEN) returns a signed blob URL that serves the in-progress
run's artifact. Download now prefers the runtime API when runner credentials
are present and falls back to REST for artifacts from other (completed) runs,
so both same-run and cross-run handoffs work.
Adds unit tests for the runtime download path, the REST fallback, and the
GetSignedArtifactURL Twirp call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): resolve same-run planfile downloads across jobs via runtime ListArtifacts
The first runtime-download attempt called GetSignedArtifactURL with the
downloading job's backend IDs, which only addresses artifacts uploaded by
that same job. A planfile uploaded by an earlier plan job therefore wasn't
found, the store fell back to REST, and REST can't read an in-progress run
("zip: not a valid zip file").
Mirror actions/download-artifact: first ListArtifacts (run-scoped, across all
jobs) to find the artifact and the backend IDs of the job that uploaded it,
then GetSignedArtifactURL with those IDs. Both runtime Twirp calls share a
small postRuntimeJSON helper. Cross-job, same-run plan-then-apply now works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ci): automatic, configurable planfile drift verification on deploy
Make native-CI planfiles work end-to-end. When planfile storage is configured
and `atmos terraform deploy` runs under CI, Atmos now automatically downloads
the stored plan, re-plans, compares them (semantic JSON plan-diff), and applies
the verified plan — failing on drift by default. Previously `deploy --ci`
downloaded the stored plan and then ignored it unless `--verify-plan` was passed.
- Config: `components.terraform.planfiles.verify` (fail | warn | off; default
fail under CI). Per-run overrides `--verify-plan` / `--no-verify-plan`.
Precedence: CLI > config > CI default. `warn` proceeds on drift; `off` skips
verification and the stored-plan download.
- `planfile.ResolveVerifyMode` resolves the mode at the download hook and the
deploy gate; `VerifyPlanfile` takes the mode, with the post-diff decision in
`finalizeVerification` (unit-tested fail/warn/match).
- Verification stays on `deploy`, not `apply`: deploy re-runs plan (so a fresh
plan exists to diff against), apply never re-plans.
Docs explain why a naive diff fails (plans legitimately vary between review and
apply) and why semantic comparison is the material improvement. Adds a separate
planfile-verify-e2e workflow, a blog post, and a roadmap entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ci): clarify deploy reconciles a fresh plan rather than replaying the stored one
Address review feedback: the default deploy flow does not apply the stored
binary planfile — it generates a fresh plan with the apply-time identity and
state, reconciles it against the reviewed plan via the diff, and applies the
fresh plan only on a match. This avoids the most common saved-plan failure (the
role that created the plan differing from the role applying it). Direct replay
of the stored planfile remains available via --from-plan / --planfile.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ci): simplify the "reconcile, don't replay" blog section
Tighten the wording per review feedback — same point (deploy re-plans and
applies the fresh plan rather than replaying the stored binary, avoiding the
plan-time vs apply-time role mismatch), said in far fewer words.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ci): correct the saved-plan failure rationale (encoded role vs base creds)
Terraform encodes the assumed role (provider assume_role) into the binary plan,
so "a plan built under one role won't apply under another" was imprecise. The
real brittleness of replaying a saved plan is that it goes stale when state
moves, and that the base credentials authenticating it come from the apply
environment, not the plan — so a plan built on a PR can fail to apply on merge.
Reconciling via a fresh plan avoids both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ci): fix the "frozen artifact" contradiction in the planfile blog
A planfile IS a frozen artifact, so opening with "a plan isn't a frozen
artifact" was self-contradictory. Reframe: the planfile is a frozen snapshot,
but the plan it represents never is — details shift between review and apply
and you adjust course without changing what you set out to do, which is exactly
why a naive diff fails and a semantic one is needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ci): trim wordy 'instead of perpetually red' from planfile blog
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ci): cover runtime download error paths for github/artifacts store
Add unit tests for downloadViaRuntime (invalid token, artifact-not-found,
signed-URL error, empty URL, blob failure), fetchBlob (non-200, transport
error), and ListArtifacts JSON-decode error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ci): configurable on_missing planfile verification; self-asserting E2E
Make native-CI planfile verification carry its own contract so the E2E
workflow no longer needs `--ci` flags or log-grep scaffolding.
- Define `components.terraform.planfiles.on_missing` (fail|warn|off) for the
previously-undefined "no stored plan found" case. Defaults to tracking the
resolved `verify` mode, so a fail-by-default CI deploy fails loudly instead
of silently applying an unverified fresh plan. A green deploy now proves
verification ran. Adds ResolveMissingMode + handleMissingStoredPlan and the
ErrStoredPlanfileMissing sentinel.
- Strip planfile-verify-e2e.yml: drop redundant `--ci` (auto-detected), reduce
the verify job to one line, and use the deploy's native non-zero exit for the
drift job (no set +e / $? / grep). Add a `planfile list -f json` step that
asserts the upload via structured output.
- Add empirical list coverage: list_integration_test.go drives the real local
filesystem store through createStore -> store.List() across component/stack/
SHA/all filters, asserting contents by value.
- Docs: new "Missing stored plan" section; update deploy + planfile-storage
pages; amend the planfile blog and roadmap milestone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ci): model planfile missing-plan policy as `required: *bool`
The missing-stored-plan condition is binary ("must a reviewed plan exist?"),
not a 3-way gradient, so the `on_missing: fail|warn|off` enum from dcd26d4 is
replaced with `components.terraform.planfiles.required` (*bool), matching Atmos's
established `Required` idiom (SecretDeclaration.Required, CommandArgument.Required).
- schema: PlanfilesConfig.OnMissing (enum) -> Required (*bool); nil = unset.
- planfile.ResolveMissingMode -> IsPlanRequired: explicit required wins; unset
tracks verify strictness (required only when verify resolves to fail);
verify=off short-circuits to not-required (also covers --no-verify-plan).
- handleMissingStoredPlan now fails when a plan is required, else logs+proceeds.
- The boolean inherently closes the prior "invalid enum silently disables the
guard" gap (a *bool can't be invalid).
- Tests, docs, blog, and roadmap updated from on_missing -> required.
Note: the planned fetch-error-vs-absent hardening is deferred. CI hook errors are
swallowed by design at two layers (executor.go and runCIHooksForDeploy), so making
a download fetch-failure fatal needs the hook to signal the failable deploy path
via shared info rather than returning an error -- a separate change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): correct planfile list assertion in E2E (--format, length check)
The `List the uploaded planfile` step failed with "unknown shorthand flag 'f'":
`planfile list` registers `--format` with no `-f` shorthand. It also keyed JSON
objects by uppercase column headers (COMPONENT/STACK/...), so the prior
`select(.component==...)` would never have matched even with the flag fixed.
Verified locally against a real local-store upload: switch to `--format=json`
and assert `jq -e 'length > 0'`. The command already filters by component+stack
+SHA, so a non-empty array proves the upload landed; an absent planfile renders
`null`, which `length > 0` correctly fails on. This is casing-independent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ci): single tri-state --verify-plan flag; cover deploy verify paths
Replace the negated --no-verify-plan flag with a tri-state --verify-plan
(--verify-plan / --verify-plan=false), matching the --identity/--pager
Changed()+viper.IsSet() pattern. Drop ATMOS_TERRAFORM_NO_VERIFY_PLAN; disabling
is now ATMOS_TERRAFORM_VERIFY_PLAN=false. TerraformRunOptions.VerifyPlan becomes
*bool; extract a testable resolveVerifyPlanMode(cmd, viper) helper.
Add tests to lift patch coverage past the 80% gate: PlanfileVerifyMode.IsValid,
VerifyPlanCLIOverride + tri-state parsing, resolveVerifyPlanMode,
verifyStoredPlanForDeploy (self-contained temp atmos project), onBeforeDeploy
download gating, and postRuntimeJSON error branches. Fix hardcoded /tmp planfile
paths in finalizeVerification tests to use t.TempDir().
Update deploy docs, env-var reference, planfile/CI docs, blog, and PRDs to
--verify-plan=false.
* fix(ci): resolve --verify-plan without viper.IsSet (fixes drift-fails E2E)
The tri-state --verify-plan resolution used viper.IsSet("verify-plan"), but the
flag binding registers a default via viper.SetDefault, and viper.IsSet returns
true whenever a default exists. So an unset flag was read as --verify-plan=false
=> PlanfileVerifyOff: the before.deploy hook skipped the stored-plan download and
the missing-plan check resolved to "not required", letting a drifted deploy apply
a fresh plan instead of failing. This broke the "deploy (drift fails)" E2E.
Resolve the override from cmd.Flags().Changed plus a direct
ATMOS_TERRAFORM_VERIFY_PLAN read, in both the PreRunE download-gate path and the
RunE verify path. Drop the viper-based TerraformRunOptions.VerifyPlan /
VerifyPlanCLIOverride.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): skip deploy planfile verification when storage is not configured
verifyStoredPlanForDeploy ran on every `terraform deploy`, so a deploy of a
component with no planfile storage configured emitted a spurious "No stored
planfile found to verify; applying a fresh plan without verification" warning
(and, in CI, could resolve the missing-plan requirement). This broke the
atmos_terraform_deploy_locked_component golden snapshot.
Gate verification on planfile.StorageConfigured, mirroring the
before.terraform.deploy download hook's existing storage check: verification is
opt-in via planfile storage, so without it deploy proceeds untouched. Add a
regression test plus storage to the verifyStoredPlanForDeploy test fixtures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ci): correct misleading "apply never re-plans" claim
`terraform apply` with no planfile still computes a plan internally, so "applies
an existing plan or config directly and never re-plans" was inaccurate. Reframe
the deploy-vs-apply distinction around what Atmos captures: `deploy` runs a
discrete plan step that produces a fresh planfile to diff against the stored one,
while `apply` either applies a planfile passed via --planfile or plans-and-applies
in one step — so Atmos has no separately-captured fresh plan to verify against.
Fixes the apply command docs, the planfile-verification blog post, and the PRD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(flags): add IsBoolFlagExplicitlySet for tri-state flag detection
resolveVerifyPlanMode read os.LookupEnv("ATMOS_TERRAFORM_VERIFY_PLAN") and the
raw cobra flag directly, hardcoding the env var name in cmd/terraform and
bypassing the pkg/flags infrastructure. Move the tri-state detection into the
flags layer: StandardParser.IsBoolFlagExplicitlySet(cmd, name) checks
cmd.Flags().Changed for the CLI value and falls back to os.LookupEnv over the
flag's registered env vars (from the parser registry, so the env var name lives
only in the deploy.go flag definition).
This deliberately avoids viper.IsSet, which returns true for any key with a
registered SetDefault — the false positive that previously collapsed the unset
case to off and disabled verification by default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ci): address CodeRabbit review on planfile verification docs
- terraform-deploy.mdx, planfiles.mdx, roadmap.js: say "applies the fresh plan
after verification" instead of "the verified plan" — the reconcile applies the
freshly generated plan, not a replay of the stored one.
- environment-variables.mdx: move ATMOS_TERRAFORM_VERIFY_PLAN out of "Context
Variables" (Atmos-populated shell context) into "Terraform Integration", where
user/CI-provided config overrides belong.
- roadmap.js: replace the non-existent `--no-verify-plan` with `--verify-plan=false`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Andriy Knysh <aknysh@users.noreply.github.com>1 parent c61e38b commit 2c6912a
36 files changed
Lines changed: 2563 additions & 141 deletions
File tree
- .github/workflows
- actions/github-runtime
- cmd/terraform
- planfile
- docs/prd/native-ci
- framework
- terraform-plugin
- errors
- internal/exec
- pkg
- ci
- artifact/github
- plugins/terraform
- planfile
- flags
- schema
- tests/fixtures/scenarios/planfile-artifacts-e2e
- components/terraform/mock
- stacks
- catalog
- deploy
- website
- blog
- docs
- ci
- cli
- commands/terraform
- components/terraform
- src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
6 | 13 | | |
7 | 14 | | |
8 | 15 | | |
| |||
56 | 63 | | |
57 | 64 | | |
58 | 65 | | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
59 | 83 | | |
60 | 84 | | |
61 | 85 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | | - | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
7 | 11 | | |
8 | 12 | | |
9 | 13 | | |
| |||
13 | 17 | | |
14 | 18 | | |
15 | 19 | | |
16 | | - | |
| 20 | + | |
17 | 21 | | |
18 | 22 | | |
19 | 23 | | |
| |||
29 | 33 | | |
30 | 34 | | |
31 | 35 | | |
32 | | - | |
| 36 | + | |
33 | 37 | | |
34 | 38 | | |
35 | 39 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
124 | | - | |
| 124 | + | |
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
| |||
0 commit comments