active_force 0.28.0 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/release/SKILL.md +91 -0
- data/CHANGELOG.md +3 -0
- data/lib/active_force/sobject.rb +1 -1
- data/lib/active_force/version.rb +1 -1
- data/spec/active_force/sobject_spec.rb +110 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9282bb2814d5d90acdad820f72977aee58dfa17af2feff228e05a68c77b1cece
|
|
4
|
+
data.tar.gz: 34e234b1cf52121f65853f9fea12c438b344a800f5d3623931b2d5b45230dd43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e056683a3151f3528e3ee1d5bae676ae6f0366ce4fdf4af718b28366c8fbd2114401860fe781fddff0722b40db97ca16f1560eac34e5bb9b5f9cad72a74311e3
|
|
7
|
+
data.tar.gz: ca35e7aacf091512287c65e4a7e816f0fe1eb027716dd67d498b6fe4c9c8d6a4156b2b54898e9c1b90d48b7b8e8abb095eecaa4f140b6777a0905ad88093b9fc
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release
|
|
3
|
+
description: Release a new version of the active_force gem. Use when asked to cut, prepare, or publish a release. Phase 1 (default) prepares the release PR; phase 2 (`publish`, after the PR is merged) builds and pushes the gem and creates the GitHub release.
|
|
4
|
+
argument-hint: "[version|publish] [--dry-run]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Releasing active_force
|
|
8
|
+
|
|
9
|
+
Two phases, split by the release PR being reviewed and merged. The argument is either a version or `publish`, never both.
|
|
10
|
+
|
|
11
|
+
- A version (`0.28.0`, no `v`) runs phase 1 with that version.
|
|
12
|
+
- No argument runs phase 1 and follows "Choosing the version" below.
|
|
13
|
+
- `publish` runs phase 2. It takes no version: it reads `VERSION` from `lib/active_force/version.rb` on an up-to-date `main`, which is the merged release.
|
|
14
|
+
|
|
15
|
+
`--dry-run` can be added to either form. See "Dry run" below.
|
|
16
|
+
|
|
17
|
+
## Dry run
|
|
18
|
+
|
|
19
|
+
With `--dry-run`, work through the phase as written but change nothing: describe what each step would do, with the real values filled in, and stop at the end. Print the plan as a numbered list in the same order as the steps, marking each as `would do`, `would skip (already done)` or `blocked (reason)`.
|
|
20
|
+
|
|
21
|
+
Only these commands are allowed:
|
|
22
|
+
- Read-only git: `git status`, `git fetch`, `git log`, `git diff`, `git show origin/main:<path>`, `git ls-remote --tags origin <tag>`. Do not switch branches or pull; read `main` through `origin/main` instead.
|
|
23
|
+
- Read-only GitHub: `gh pr list`, `gh pr view`, `gh release list`, `gh release view`, and the `generate-notes` API call (it only returns text).
|
|
24
|
+
- `gem list -r`.
|
|
25
|
+
|
|
26
|
+
Do not run `git switch`, `git pull`, `git commit`, `git push`, `gem build`, `gem push`, `gh pr create`, `gh release create`, or edit any file. If no version was given, still propose one and ask for it: the plan depends on it. Do not ask for the confirmations that precede `gem push` or `gh release create`; show what you would ask instead.
|
|
27
|
+
|
|
28
|
+
What to show:
|
|
29
|
+
- **Phase 1:** the chosen version, the branch name, the PR title, the exact `CHANGELOG.md` diff and `version.rb` change as they would be written, any `TBD` links, and the result of the already-released check.
|
|
30
|
+
- **Phase 2:** the version read from `origin/main`, whether the release PR is merged (and its number), the already-done check for rubygems, the GitHub release and the tag, the commands that would build and push, and the **final release notes** with the release PR's bullet removed.
|
|
31
|
+
|
|
32
|
+
End by saying nothing was changed, and that running the same command without `--dry-run` would do it for real.
|
|
33
|
+
|
|
34
|
+
## Choosing the version
|
|
35
|
+
|
|
36
|
+
Read the `## Not released` section of `CHANGELOG.md` and the current `ActiveForce::VERSION` in `lib/active_force/version.rb`.
|
|
37
|
+
|
|
38
|
+
- If there are no entries under `Not released`, stop and say there is nothing to release.
|
|
39
|
+
- Otherwise propose a bump (new feature or behavior change = minor, fixes only = patch, breaking change = major; this gem is 0.x so breaking changes are also a minor bump) with a one-line reason, and **ask the user to confirm or give a different version**. Never pick one silently.
|
|
40
|
+
|
|
41
|
+
## Phase 1: prepare the release PR
|
|
42
|
+
|
|
43
|
+
1. Require no modified or staged tracked files (`git status --untracked-files=no` is empty); untracked files don't matter. `git switch main && git pull`.
|
|
44
|
+
2. Check the `Not released` entries for placeholder links (`pull/TBD`). If any are found, list them and ask the user before continuing.
|
|
45
|
+
3. Refuse if `{version}` is already released: a git tag `{version}` exists, or `gem list -r -e active_force --all` includes it. `{version}` must also be greater than the current `VERSION`.
|
|
46
|
+
4. `git switch -c release_{version}`.
|
|
47
|
+
5. Set `VERSION = '{version}'` in `lib/active_force/version.rb`.
|
|
48
|
+
6. In `CHANGELOG.md`, insert a new `## {version}` heading directly under `## Not released`, so the existing entries fall under it, and leave `## Not released` at the top, empty. Result:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
# Changelog
|
|
52
|
+
|
|
53
|
+
## Not released
|
|
54
|
+
|
|
55
|
+
## {version}
|
|
56
|
+
- ...entries...
|
|
57
|
+
```
|
|
58
|
+
7. Do not touch `Gemfile.lock` (gitignored) and never `git add` `*.gem` files. Stage only `version.rb` and `CHANGELOG.md`.
|
|
59
|
+
8. Commit as `Release {version}`, push, and open a PR titled `Release {version}`. **Do not use the Linear `KEY-N | TYPE | title` format for release PRs.** The body can be the changelog entries being released.
|
|
60
|
+
9. Report the PR URL and stop. Someone else approves and merges it. Do not merge it.
|
|
61
|
+
|
|
62
|
+
## Phase 2: publish (only after the release PR is merged)
|
|
63
|
+
|
|
64
|
+
Naming, matching past releases: the git tag is `{version}` (no `v`) and the GitHub release title is `v{version}`.
|
|
65
|
+
|
|
66
|
+
1. `git switch main && git pull`. Read `{version}` from `version.rb`.
|
|
67
|
+
2. Confirm the release PR is merged: `gh pr list --head release_{version} --state merged --json number,title`. If it isn't, stop and say so. This works after the branch is deleted. Keep the PR number for step 6.
|
|
68
|
+
3. **Check what is already done**, so a rerun resumes instead of failing partway. Report the result to the user, skip the steps that are done, and continue with the rest:
|
|
69
|
+
- On rubygems: `gem list -r -e active_force --all` includes `{version}` -> skip build and push (steps 4-5).
|
|
70
|
+
- GitHub release: `gh release view {version}` succeeds -> skip notes and release (steps 6-7). If it succeeds, tell the user the release already exists and give the link.
|
|
71
|
+
- A tag `{version}` exists (`git ls-remote --tags origin {version}`) with no GitHub release: stop and ask the user. The tag may point at the wrong commit, so don't create a release on it unprompted.
|
|
72
|
+
- If everything is done, say so and stop.
|
|
73
|
+
4. `gem build active_force.gemspec`.
|
|
74
|
+
5. **Ask for explicit confirmation, then** `gem push active_force-{version}.gem`. A push cannot be undone. If rubygems needs an MFA code, ask the user to run it themselves with `! gem push active_force-{version}.gem`.
|
|
75
|
+
6. Generate release notes and drop the release PR's own entry:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
gh api repos/{owner}/{repo}/releases/generate-notes \
|
|
79
|
+
-f tag_name={version} -f previous_tag_name={previous_tag} -f target_commitish=main \
|
|
80
|
+
--jq .body
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`{previous_tag}` is `gh release list --limit 1 --json tagName --jq '.[0].tagName'`. Remove the bullet that links to the release PR's number (from step 2); do not match on its title. Keep everything else, including the "Full Changelog" line.
|
|
84
|
+
7. Show the user the final notes and ask to confirm. Then:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
gh release create {version} --target main --title "v{version}" --notes-file <file>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This creates the tag.
|
|
91
|
+
8. Report the rubygems and GitHub release links. Leave the built `.gem` file alone (gitignored).
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Not released
|
|
4
4
|
|
|
5
|
+
## 0.28.1
|
|
6
|
+
- `modified_attributes` computes the changed attribute names once instead of once per attribute, making it linear in the number of fields. The result is unchanged. (https://github.com/Beyond-Finance/active_force/pull/126)
|
|
7
|
+
|
|
5
8
|
## 0.28.0
|
|
6
9
|
- Add `delete` / `delete_all` (class, relation and instance) using the Composite sObject Collections API. Ids are deleted in chunks of 200, each chunk all-or-none, and the first failing chunk raises `ActiveForce::DeleteFailed` with the Salesforce `errors` and the `deleted_count` from earlier chunks, which stay deleted. Nothing is retried, and ids that are already deleted or never existed are errors like any other. (https://github.com/Beyond-Finance/active_force/pull/119)
|
|
7
10
|
- `delete` raises `ArgumentError` before sending any request when an id's key prefix belongs to another object, so `Account.delete(a_contact_id)` no longer deletes the Contact. The prefix comes from `Model.key_prefix`, read from describe once per class and cached. Objects with no key prefix are not checked, and objects that share a prefix (`ContentDocument` and `ContentNote`) can still be mixed up. (https://github.com/Beyond-Finance/active_force/pull/122)
|
data/lib/active_force/sobject.rb
CHANGED
data/lib/active_force/version.rb
CHANGED
|
@@ -547,6 +547,116 @@ describe ActiveForce::SObject do
|
|
|
547
547
|
end
|
|
548
548
|
end
|
|
549
549
|
|
|
550
|
+
describe '#modified_attributes' do
|
|
551
|
+
context 'with a new instance' do
|
|
552
|
+
it 'returns an empty hash when nothing has been assigned' do
|
|
553
|
+
expect(Whizbang.new.modified_attributes).to eq({})
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
it 'returns only the assigned attributes' do
|
|
557
|
+
instance = Whizbang.new(text: 'some text', boolean: false)
|
|
558
|
+
|
|
559
|
+
expect(instance.modified_attributes).to eq('text' => 'some text', 'boolean' => false)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
it 'returns the cast values of the assigned attributes' do
|
|
563
|
+
instance = Whizbang.new(date: '2010-01-01', percent: '21')
|
|
564
|
+
|
|
565
|
+
expect(instance.modified_attributes).to eq('date' => Date.new(2010, 1, 1), 'percent' => 21.0)
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
context 'with a built instance' do
|
|
570
|
+
let(:instance) { Whizbang.build sobject_hash }
|
|
571
|
+
|
|
572
|
+
it 'returns an empty hash when nothing has changed' do
|
|
573
|
+
expect(instance.modified_attributes).to eq({})
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
it 'returns only the changed attributes with their new values' do
|
|
577
|
+
instance.text = 'changed text'
|
|
578
|
+
instance.checkbox = true
|
|
579
|
+
|
|
580
|
+
expect(instance.modified_attributes).to eq('checkbox' => true, 'text' => 'changed text')
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
it 'excludes attributes assigned their current value' do
|
|
584
|
+
instance.text = 'Hi there!'
|
|
585
|
+
instance.percent = 21
|
|
586
|
+
|
|
587
|
+
expect(instance.modified_attributes).to eq({})
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
it 'excludes attributes changed and then restored to their original value' do
|
|
591
|
+
instance.text = 'changed text'
|
|
592
|
+
instance.text = 'Hi there!'
|
|
593
|
+
|
|
594
|
+
expect(instance.modified_attributes).to eq({})
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
it 'includes attributes changed to nil' do
|
|
598
|
+
instance.text = nil
|
|
599
|
+
|
|
600
|
+
expect(instance.modified_attributes).to eq('text' => nil)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
it 'includes attributes mutated in place' do
|
|
604
|
+
instance.text << ' Bye!'
|
|
605
|
+
|
|
606
|
+
expect(instance.modified_attributes).to eq('text' => 'Hi there! Bye!')
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
it 'excludes attributes that were not selected' do
|
|
610
|
+
expect(Whizbang.attribute_names).to include('estimated_close_date', 'updated_from')
|
|
611
|
+
expect(instance.modified_attributes.keys).not_to include('estimated_close_date', 'updated_from')
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
it 'includes an unselected attribute once it is assigned' do
|
|
615
|
+
instance.updated_from = 'Rails'
|
|
616
|
+
|
|
617
|
+
expect(instance.modified_attributes).to eq('updated_from' => 'Rails')
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
it 'returns string keys in attribute declaration order' do
|
|
621
|
+
instance.percent = 42
|
|
622
|
+
instance.text = 'changed text'
|
|
623
|
+
instance.checkbox = true
|
|
624
|
+
|
|
625
|
+
expect(instance.modified_attributes.keys).to eq(%w[checkbox text percent])
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
context 'after persisting changes' do
|
|
630
|
+
let(:instance) { Territory.new(id: '1') }
|
|
631
|
+
|
|
632
|
+
it 'returns an empty hash after create' do
|
|
633
|
+
new_instance = Territory.new(name: 'Walter')
|
|
634
|
+
allow(client).to receive(:create!).and_return('1')
|
|
635
|
+
|
|
636
|
+
new_instance.create!
|
|
637
|
+
|
|
638
|
+
expect(new_instance.modified_attributes).to eq({})
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
it 'returns an empty hash after update' do
|
|
642
|
+
allow(client).to receive(:update!).and_return(true)
|
|
643
|
+
|
|
644
|
+
instance.update!(name: 'Walter')
|
|
645
|
+
|
|
646
|
+
expect(instance.modified_attributes).to eq({})
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
it 'returns an empty hash after reload' do
|
|
650
|
+
allow(client).to receive(:query).and_return([Restforce::Mash.new(Id: '1', Name: 'Jeff')])
|
|
651
|
+
instance.name = 'Walter'
|
|
652
|
+
|
|
653
|
+
instance.reload
|
|
654
|
+
|
|
655
|
+
expect(instance.modified_attributes).to eq({})
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
550
660
|
describe 'logger output' do
|
|
551
661
|
let(:instance){ Whizbang.new }
|
|
552
662
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_force
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.28.
|
|
4
|
+
version: 0.28.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eloy Espinaco
|
|
@@ -159,6 +159,7 @@ extensions: []
|
|
|
159
159
|
extra_rdoc_files: []
|
|
160
160
|
files:
|
|
161
161
|
- ".circleci/config.yml"
|
|
162
|
+
- ".claude/skills/release/SKILL.md"
|
|
162
163
|
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
|
163
164
|
- ".github/ISSUE_TEMPLATE/feature_request.md"
|
|
164
165
|
- ".gitignore"
|