Updated 8 November 2026. Read this before touching the deployed desk, then README.md for how it works.
State: the desk is live and proven end to end in production (test ticket
PSA-2616, 25 October). Lenovo: Jennifer’s first real order is on it (K871293632
on PSA-2349, waiting to ship). Amazon is live (#257): the Google Admin rule copies
Jennifer’s and Joe’s Amazon order mail to procurement@, and the first real Amazon
order’s shipping update was approved from the desk on 2 November (PSA-2643). Okta
sign-in, and every Jira change approved by the buyer, made by the desk’s own
Atlassian service account. The order ledger, #260’s order prices, Ask the desk and
Reports are live (00030-kez, 8 November). Next: the tax status of the six
entities missing from Business Ops’ sheet (Reports, below), ABM’s three secrets and
its preflight (“Next session: Amazon, then Apple”), asking Robbie which reports she
makes today (to add as starters), PSA-2643’s delivery, then roles before Kelly goes
on the access list.
Where the desk is going (set 7 November): ROADMAP.md. The order ledger first, then roles, Ask the desk, Robbie’s reports, Apple with ABM, receipts.
00030-kez, #273)Goal 4’s first cut; how it works is in the README, “Reports”. A Reports tab over PSA
and the ledger, built after Adam chose a separate reporting page over tables under
Ask’s answers (built and dropped the same day, a78a42d). Ask’s answers now link to
the same figures in Reports instead.
scripts/try-report.mjs, read only): October, 94 requests (one rejected, no
cost); For-profit $237,489.50, Non-profit $32,782.38, Not classified $80,199.55,
the same as Ask’s figures to the cent; median 6.1 days to close. The year to
7 November: 776 requests in about 2 s (one search per month), 27 rejected left out,
spend $2,718,086.67; Not classified $853,554.62, almost all Aperium. The ledger: 5
orders since 20 October, $6,611.06, four linked to their tickets.purchase_requests now returns spend with and without them.
PSA-1824 is Shipped but still carries “Declined” from before it was reopened, so
only a closed request counts as rejected (requestState in jira-client.mjs).jira-client.mjs now reads resolutiondate and resolution on every search
(days to close, rejected), two more fields.main 4ace109) also carried Ask’s new look, tax status and the fixes
from Copilot’s and Claude’s reviews. Live as 00030-kez on 8 November, no new
permission or setting: the runtime already reads PSA and the ledger. Checked: the
tag URL gave 308 on / and 403 on /api/session and /api/report, the startup
line read “Okta sign-in, mailbox on, Jira writes on, Ask on (claude-opus-5-5)”, and
signed out on the live URL / is 200, /app.js, /api/orders, /api/report and
/api/ask are 401. Rollback: route to 00028-qux (Ask, no Reports).00028-qux)Goal 3; how it works is in the README, “Ask the desk”. #272 (main 41b5b65), live as
00028-qux with PROCUREMENT_ANTHROPIC_API_KEY=purchasing-desk-anthropic-api-key:2
(startup: “Ask on (claude-opus-5-5)”). The key is its own: a Claude Console service
account purchasing-desk (Developer, not Admin) whose key purchasing-desk-cloud-run
is also kept in 1Password. Version 1 of the secret held copied command text (the
clipboard held the commands, not the key) and is destroyed.
scripts/try-ask.mjs, 13-17 s each): “Lenovo
laptops this month” (none; it searched ThinkPad, Yoga and IdeaPad and checked PSA),
“Amazon spend since October” (it declined an Amazon-only figure, gave PSA’s $80,946
as an upper bound and named the mixed tickets), “quietest open requests” (PSA-1496
and PSA-1610, the two $540,795 DGX Stations, untouched since July and August; it
flagged them as a possible duplicate). The third first asked for more than two
years of requests and was refused; open requests are now unbounded (45 open, none
older than July 2026).roles/bigquery.jobUser on the
project and Data Viewer (READER) on purchasing_desk for purchasing-desk-runtime@.
Proven live the same evening: Adam asked “What Amazon items did we order this week,
and what did each cost?” and the desk answered from the ledger (4 items, $72.23
before tax, each with its ticket, one order on no ticket yet); desk.ask shows
ordered_items, no errors, 2 rounds, 9.7 s, prompt cache read on the second round.jsonPayload.event="desk.ask" (who, tools, filters,
tokens); failures desk.ask_failed.Goal 1 of the roadmap; how it works is in the README, “Order ledger”. Adam’s call (7 November): BigQuery in the desk’s own project, separate from Harry’s CSAT, so Ask the desk answers item and price questions from every order the mailbox sees, not only the ones linked from the desk.
bq is broken
on his desktop): dataset purchasing_desk (US, like harry_csat); log sink
purchasing-desk-ledger, --use-partitioned-tables, filter
resource.type="cloud_run_revision" AND resource.labels.service_name="purchasing-desk-pilot" AND jsonPayload.event="desk.ledger";
its writer service-343992350788@gcp-sa-logging.iam.gserviceaccount.com is
WRITER on that dataset only. The runtime account got nothing.server/ledger.mjs, its two calls in desk.mjs (mailbox reads and
drop-ins), ledger_bigquery.sql. 139 tests (6 new), 58 UI checks. The three views
were run in BigQuery over lines made from the fixtures: duplicates collapse to one
email, a re-sent confirmation is still one order, the mailbox’s copy wins even over
a newer edited drop-in, an order known only from a drop-in stays marked upload,
items come from confirmations only (the orders view came from #270’s review).00026-san (#270, main 4fd24a8), 7 November.ledger_bigquery.sql. Totals beside
PSA: K871293632 $6,532.79 = PSA-2349; 114-4594784 $14.75 = PSA-2750; 114-8691035
$34.83 = PSA-2748; 114-4342416 $12.92 on PSA-2728, whose $1,318.33 holds two vendors’
orders (the case the ledger is for); 114-8633789 $15.77, on no ticket yet. Each read
takes at most 25 new messages, so the rest of the mailbox (Lenovo since 20 October,
Amazon since 29 October) arrives as the desk is used.bigquery.jobUser on the project and READER on
this dataset for purchasing-desk-runtime@ (Adam grants both: Ask the desk, step 3).00024-duj (from main cd17dde, 30 October; read back
7 November), recorded in “Live deployment” below.114-2998969-7076254, ordered 28 October, the day before
the Google Admin rule): its confirmation never reached procurement@, so Joe linked
it by hand and the desk counted that step as done. On 2 November the buyer approved
the shipping update from the desk: comment with Amazon_TBA…, ETA Monday 11/2,
moved to Shipped. Delivery and close are next. Not yet seen on real mail: an order
placed after the rule (the desk’s own confirmation step), several items or packages,
a cancellation. ITPROJ-1415 has the 2 November update.feat/procurement-order-prices, not mergedAmazon’s shipped and delivered emails print no price, so an order whose confirmation never reached procurement@ showed “—” (PSA-2643). Buyers type the cost into the ticket by hand; in the last 180 days only 3 of 253 Amazon tickets have none (PSA-1482, PSA-1120, PSA-997).
priceOf in orders.mjs, shown on the card and the
panel): the confirmation email; else one the buyer added on the desk; else the
ticket’s own Total Order Cost, labelled “From the ticket”, or “Ticket total, other
orders too” when the title tracks other orders. None: “Needs a price”.price: cents, by, at, costBefore,
fields); the timeline shows “Price added”.Moved to ROADMAP.md, goals 1 (items on the ticket) and 3 (Ask the desk).
Built (29 October, night), all on the branch, uncommitted:
server/amazon-mail.mjs, from Joe’s real ordered, shipped and delivered emails for
114-3671313-0376252 (PSA-2645; the .eml files are in Adam’s ~/Downloads, never
the repo; fixtures are invented copies). It reads the plain-text part (the HTML
cuts item names short and wraps every link in Amazon’s click tracker; readMessage
now returns text too). One sender per kind, each verified (DKIM amazon.com and
DMARC pass in all three real emails): auto-confirm@ ordered,
shipment-tracking@ shipped, order-update@ delivered. Amazon prints no order
date or part numbers and only a Grand Total: the order date is the confirmation’s
sent day (Pacific), and “Arriving tomorrow 9 AM – 12 PM” is resolved against the
email’s own date into the buyers’ wording (“Thursday, 10/29”, windows as “Friday,
10/30 - Monday, 11/2”).USPS_<n> to
tools.usps.com/tracking/<n>, UPS_<n> to ups.com/track?track=yes&trackNums=…,
FedEx_<n> to fedex.com/wtrk/track/?tracknumbers=…; TBA… (Amazon Logistics)
links Amazon’s tracker. Empty, it posts Amazon_<order> to Amazon’s tracker, as
Joe does. The number is kept on the shipment’s record, so the delivered comment
repeats the same link. Next step: the API fills it in (“Amazon Business API” below).Ordered, Shipped, Delivered), keeping its other notices out.hosted-server.mjs; now any vendor’s), and a card’s action clicked right
after a previous approval on the same order reopened its panel without starting the
review (Orders.jsx; each opening now counts). From review (Copilot on #257): the
order search passed every order number in one Jira query, which takes at most 100
(now batched), and only the first 40 linked tickets were read, so an order on any
other showed as “Needs a ticket” (closed tickets now keep their search copy; open
ones and fresh links are read directly, up to 60); a package whose tracker can’t
be read keeps the order from counting as delivered; “Arriving Monday - Friday”
sent midweek is next Monday to next Friday; and a split order is due when its last
package still on the way is, never before the confirmation’s date.Next: commit; one PR (the Amazon build, its UI wording and docs together);
merge; the usual deploy (production is still 00022-goq); then a pilot on one real
Amazon order with Jennifer or Joe. The rule’s first copy arrives with their next
Amazon order: check procurement@ has it, and that the desk reads it (the copy must
still pass the sender check; drop it in if in doubt).
Before the build (how we got here). The vendor framework (Step 0 below) and these notes are on
main through #255, merged but not deployed: production is still 00022-goq.
The framework changes nothing on its own, so it ships with the Amazon build’s deploy
(a deploy of main before then is safe). The Amazon build starts on a new branch from
main (“Picking this up on another machine” below, <topic> = amazon) and goes
out as one PR with its UI wording and docs. That branch exists (worktree
.worktrees/procurement-amazon, not pushed yet) with the part that needs no samples:
reading Amazon’s PSA titles (“What Amazon changes” below). Amazon’s order emails reach only the
buyer who placed the order: procurement@ gets Amazon’s promotions and the Prime
billing statement, and Amazon Business has no shared notification list (Adam looked
on 29 October; Amazon’s documentation agrees). So a Google Workspace rule copies
them. Adam is not a super admin in hillspire.com, so a coworker who is creates it.
Purchasing Desk: copy Amazon order mail to procurement@.Sender header, Contains text amazon.com; and
Location Any envelope recipient, Matches regex
(jshon|jwen)@hillspire\.com (no anchors, in case Google tests the recipients
as one string; Jennifer and Joe; Joe’s address is confirmed by
Apple’s shipping notice, which is addressed to him).procurement@hillspire.com. Tick nothing else: not Change
envelope recipient (that redirects, and the buyers would stop getting their
mail), no route change, no spam bypass, no headers or subject prefix. Not Reject
or Quarantine.It only adds a copy, like a BCC: Jennifer, Joe, support@ and everyone else receive exactly as before, and nobody sees procurement@ on the message. Undo by disabling or deleting the rule. Tell Jennifer and Joe their Amazon mail is copied, in case a personal Amazon account uses the work address. The live desk reads only Lenovo’s senders, so nothing changes on it until the Amazon build ships.
C:\Users\adams\Downloads\amazon-samples\: an order confirmation (several items
if possible), a shipped email (one package, and a split one if it comes), a
delivered email, and a cancellation if one happens. While there, Apple’s order
confirmation for AAA6914904, which should show whether Apple’s emails carry
prices.python -I, path as an argument). Then:
server/amazon-mail.mjs with invented fixtures and tests; the adapter in
vendors.mjs with Amazon’s verified senders from the samples, orderPattern
^\d{3}-\d{7}-\d{7}$ and a mailQuery whose after: is the day the rule went
live; check the copies still pass the desk’s DKIM and DMARC check; the
vendor-neutral UI wording and the Amazon title cases (Step 0 and “What Amazon
changes” below); then one PR, the usual deploy, and a pilot on one real order
with Jennifer or Joe.00022-goq, from main 8c21521 (#248), serves 100%, no tags,
deployed 28 Oct. It reads Lenovo confirmations with bundled parts: a configured
workstation lists its support upgrade under “Items Included”, which 00020-now
refused as “Lenovo changed the layout”. Bundled parts attach to the product above
(the panel shows “Includes …”) and are not counted as items; totals read the
innermost <span>.00020-now (from main d0c129b, #242) added, on top of 00018-poz: the Today
strip (greeting; chips for orders to review, arriving today, running late, and the
quietest open request), package-style progress (a date under each step, the last
leg filling toward Lenovo’s date, red when late), late badges (“N days late”,
“Late: not shipped” with a nudge to check LenovoPRO; late orders sort first), and
Lenovo’s delivery window read as its last day (“Get it between Wed Nov 11 - Fri
Nov 13” waits for Nov 13). A request with an order on its way is never “quiet”.
Display only: no new writes or settings.00018-poz (from main baf35fa) carried #225 (the redesign: stat tiles, side
panels with timelines, shipped orders waiting under Waiting until Lenovo’s date),
#226 (build allowlist test), #228 (Jira’s 201-with-empty-body fix) and #229 (assign
the buyer on link when unassigned; setting PROCUREMENT_JIRA_ASSIGNEES).gcloud run services update-traffic purchasing-desk-pilot
--project=hillspire-procurement --region=us-central1
--to-revisions=purchasing-desk-pilot-00020-now=100 (everything but bundled parts:
K871293632 is unreadable there). Every revision before 00016-kux has the 201 bug,
so never roll back past it..eml is on Adam’s Mac in ~/Downloads, never in the repo.desk.assign_failed. Check on the first link
made from the desk.procurement@hillspire.com (“Setup”, step 4), per
buyer. Joe’s PSA-2440 names Lenovo order K871295219 but the desk never saw it, so
his emails still go to his own inbox. Until fixed, buyers drop emails in.roles/iap.httpsResourceAccessor grants (gcloud iap web
remove-iam-policy-binding --resource-type=cloud-run …); they only served the IAP
pilot rollback.purchasing-desk-jira-api-token (only
00007-les used it).main pipeline (“Goal” below) once a few real orders have
gone through.Adam wants the next two big vendors built carefully, Amazon first, then Apple.
Most PSA tickets are Amazon or Apple (see the open queue: Amazon (111-…),
Apple (AAA…)). Same rules as Lenovo, unchanged: link never create, every Jira write
previewed and approved by the buyer, fingerprinted, read-only sources.
Step 0, the framework (no behaviour change): built 28 October, merged in #255, not
deployed yet. server/vendors.mjs lists the vendors; each parser
module exports an adapter { id, name, senders, mailQuery, orderPattern, parse }, and
Lenovo is the first. The desk parses every email through parseVendorEmail (the From
address picks the parser, which still verifies DKIM and DMARC), the Gmail query is the
adapters’ mailQuery terms (one vendor gives exactly the old query), Jira’s order
search accepts any vendor’s order number and only [A-Za-z0-9-], and the vendor name
comes from the adapter. The facts each parser returns are unchanged. All 98 existing
tests pass unchanged (103 with the registry’s own), and the 37 UI checks pass.
Done on the Amazon branch (29 October): the page’s “Lenovo” copy now names the
order’s own vendor, or no vendor where it covers all of them (the drop zone, empty
states, refusal messages; “LenovoPRO” became “check the order with
Amazon (Amazon Business; Adam has access to the account).
Decided 28 October: emails, exactly like Lenovo. The Amazon Business API waits for the receipts idea. 29 October: order emails go only to the buyer who placed the order, and Amazon Business has no shared notification list, so a Google Admin rule copies them to procurement@ (“Tomorrow (30 October)” above).
.eml samples from Amazon Business: order confirmation, shipped (one and
several packages), delivered, a cancelled or partly cancelled order, and an order
with several items. Download them like the Lenovo ones; never commit them. Tests
use invented copies.111-1234567-1234567 (three groups,
digits only), several orders per ticket (Amazon (111-…, 114-…) titles are
common), one order shipping in several packages, and a real “Delivered” email (the
desk could pre-fill the delivery date; still approved).
Checked against live Jira 28 October: summary ~ "114-7099723-7304231" finds
the hyphenated number as one token (PSA-2638, and numbers after CANCELLED - or a
semicolon too); a near-miss sharing two groups finds nothing, and summary ~ "114"
finds nothing. The existing plain query works; no phrase query needed.
Real Amazon titles vary (PSA, 14–28 October): prefixes 111-, 112-, 113-,
114-; several orders per group separated by a comma, semicolon or &; vendors
separated the same three ways; leading spaces; CANCELLED - 111-… inside the
group (PSA-2453, PSA-2437); a title with no colon after the group (PSA-2587); one
with a stray ( (PSA-2424).
Built 29 October on the Amazon branch, decided by Adam: one title reader,
server/title.mjs, shared by the server and the page (it replaces src/title.js,
which had drifted from the server’s copy). A CANCELLED - … number still names
its ticket but is not open, so the close warning leaves it out. A title with no
colon keeps her format: leading Vendor (…) groups count when every reference
looks like an order number (5+ characters, 4+ digits, so “MacBook Pro (2024)”
stays a name), and a new order joins its vendor’s group with her own separator
(, ; &). Run over the 100 newest Amazon titles: 91 were right before, all
100 now. 107 tests and the 37 UI checks pass.Apple (Apple Business Manager API; a coworker gave Adam an API key).
hillspire-procurement from the file, never into chat, the repo or a ticket; only
the desk’s runtime account reads it; the client allows GET only, with a test that
fails the build otherwise (the Zoom dashboard’s pattern). Ask: who created it, with
which role, for which ABM organization (one per entity?), and whether a read-only
role exists for it. Check Apple’s current API docs for the token flow (a JWT client
assertion signed with the key, exchanged for an access token) before building.feat/procurement-abm (not merged, nothing calls it):
server/abm-client.mjs, GET only, from Apple’s current docs (README, “Apple
Business Manager”; 11 tests), and scripts/abm-preflight.mjs, run from Adam’s
desktop. Apple’s API now also lists users, apps, packages and Blueprints, and can
create or delete MDM servers and release devices from the organization
(RELEASE_DEVICES), all with the same key: the client reaches only
GET /v1/orgDevices, and a test fails the build otherwise.
Secrets (none exist yet), in hillspire-procurement, readable by Adam only
until the adapter ships (then purchasing-desk-runtime@ gets secretAccessor on
these three, nothing wider): purchasing-desk-abm-private-key (the .pem),
purchasing-desk-abm-client-id (BUSINESSAPI.<uuid>) and
purchasing-desk-abm-key-id (<uuid>), both shown in ABM next to the key. From
Adam’s own terminal, so the key never passes through chat:
gcloud secrets create purchasing-desk-abm-private-key --project=hillspire-procurement --replication-policy=automatic --data-file="<path to the .pem>";
then copy the client ID and run
powershell.exe -NoProfile -Command "[Console]::Out.Write((Get-Clipboard -Raw).Trim())" | gcloud secrets create purchasing-desk-abm-client-id --project=hillspire-procurement --replication-policy=automatic --data-file=-,
and the same with the key ID for purchasing-desk-abm-key-id. Then delete the
local .pem: Secret Manager is the copy, and a lost key is replaced in ABM.
Run: node scripts/abm-preflight.mjs --order AAA6828504 --order W1610319216 --order AFH0099144
from this folder. It reads the three secrets into memory and prints counts, order
number shapes (AAA9999999) and, per order, found or not with serials masked to the
last four; never the token, the key or a full serial.AAA6828504 (PSA-2486, an iPhone 17 for a Hillspire hire,
closed) that same AAA… number? If not, devices need another key to match
tickets. Not PSA-2655: its Apple order is peripherals,
which ABM never lists. PSA titles also carry W1610319216 (in-store pickup,
PSA-2252) and AFH0099144 (PSA-2303); whether pickups reach ABM is a second check.AAA6914904
(PSA-2603). From shipment_notification_retail_business_US@orders.apple.com
(“Apple Retail for Business”); Google’s DKIM and DMARC pass for orders.apple.com.
It carries the order number, “Date ordered” with the year, a PO number, the ship
date, the items (description, part number, quantity) and one tracking number per box
(three UPS 1Z… for three MacBooks). No prices and no delivery date. The numbers
link to Apple’s order page, store.apple.com/vieworder/<order>/<a buyer's email>,
which must never go in a comment; the plain-text part gives
store.apple.com/trackshipment<number> (untested). The carrier varies: a 27 October
notice had one 18-digit 0019… number and “Standard Shipping”, not UPS.
Apple already copies procurement@ (Joe in To; Jennifer, procurement@, support@
and Apple’s Palo Alto business team in Cc), so Apple is mailbox-first with no setup.
Still needed: Apple’s order confirmation (does it carry prices?). The real .eml is
in Adam’s desktop ~/Downloads, never in the repo.Order: framework, then Amazon discovery, then Amazon build and pilot, then Apple discovery, then Apple. Couple the steps into as few PRs as ship sensibly (Adam, 29 October: every PR notifies his coworkers’ channels): the Amazon build, its UI wording and its docs go out as one.
Done 28 October: the Today strip, package-style progress and late badges (#242).
Queued:
The rest of the 26 October list, all wanted:
j/k, a to approve).src/app.css (~1.9k lines) in the lean-down refactor.git clone git@github.com:Hillspire/SysEngineering.git # or git pull on main
cd SysEngineering
git worktree add .worktrees/procurement-<topic> -b feat/procurement-<topic> origin/main
cd .worktrees/procurement-<topic>/integrations/procurement_workspace
npm ci
npm test # 191 unit tests
npm run dev # http://127.0.0.1:4317, invented data, click "Sign in"
npm run verify # 90 checks in headless Chrome (needs Chrome installed)
gcloud auth login # only for deploys and logs; project hillspire-procurement
gh auth status # PRs; the token needs Hillspire org SSO authorized
scripts/dev-server.mjs): seven showcase orders dated relative to today, one per
state (unlinked, ship pending, in transit, due today, late shipped, late not shipped,
delivered).C:\Users\adams\Downloads\purchasing-desk-test\ on Adam’s desktop only. They are
copies of tests/fixtures/lenovo-order-*.eml with the order number changed to
K800000099, the dates set to the day and a new Message-ID; recreate them the same
way if needed. Never commit real .eml files.~/Downloads on Adam’s Mac (“Thank you for your order!.eml”; prices and addresses,
so it may have been deleted). The 9 October order is in C:\Users\adams\Downloads\
on Adam’s desktop (“Thank you for your order!.eml”, “Your order has shipped!.eml”).Amazon’s Package Tracking API returns the carrier and its tracking number:
getPackageTrackingDetails, GET /ab-tracking/2025-07-02/orders/{orderId}/shipments/{shipmentId}/packages/{packageId}
(na.business-api.amazon.com), response packageTrackingDetails.carrierDetails
with localizedCarrierName and carrierTrackingId (both optional). The shipped
email’s tracker link already carries orderId and shipmentId; whether its
packageIndex is the API’s packageId is to check (the Orders API’s getOrder
lists packages if not). With it, the ship preview’s carrier box fills itself.
Access is a process, partly Amazon’s: an Amazon Business account on the Solution
Provider Portal, a developer profile requesting the role for this API (Amazon reviews
it, with identity verification), an app client (business entity: Amazon Business)
whose Login with Amazon credentials an Amazon Business admin authorizes. Same rules
as ABM: the credentials go straight into Secret Manager in hillspire-procurement,
only the runtime reads them, the client is GET only with a test that fails the build
otherwise, and the narrowest read role. The same access later serves receipts
(Reconciliation API) for Concur. Docs: docs.business.amazon.com, “Package Tracking
overview” and “Onboarding overview”.
* name / Quantity / price blocks and several packages
pair by shipment ID, both untested on real mail: anything else is refused as
“Amazon changed the layout”, never guessed. Arrival phrases other than today,
tomorrow, a weekday or a date (“Arriving soon”) give no expected date. Item names
are as Amazon prints them, which can include the seller’s | … tail.Mailbox capacity: the newest 500 matching messages in 90 days; at Amazon’s pace that is about 75 days of mail.
purchasing-desk record. It is
harmless; nothing reads it while that title doesn’t name the order.| Setting | Current state |
|---|---|
| URL | https://purchasing-desk-pilot-343992350788.us-central1.run.app (also …-6efoouk45q-uc.a.run.app) |
| Project | hillspire-procurement (343992350788), us-central1, service purchasing-desk-pilot |
| Revision | purchasing-desk-pilot-00030-kez, 100%, from main 4ace109 (#273, image pilot:20261108-main-4ace109), deployed 8 Nov |
| Image | …/purchasing-desk/pilot@sha256:7b7748bbc36ead1433e8d17f54a5e2dfdce3ec1bf9cf66718f6850c19fa7cb37 |
| Access | IAP off, allUsers invoker, Okta OIDC in the app: landing page public, everything else needs Okta |
| Identity | Runtime purchasing-desk-runtime@, build purchasing-desk-build@, mailbox purchasing-desk-mail@ |
| Jira | Service account Purchasing Desk, purchasing-desk-jira-desk-token:1 (scoped), writes on |
| Bounds | min 0, service max 1, concurrency 8, 1 CPU, 512 MiB |
| Rollback | Route to 00028-qux (Ask, no Reports); never before 00016-kux (the 201 bug). IAP pilot: “Cutover history” below |
Users: aschembri@, rparas@, jshon@, jwen@hillspire.com. Custom domain
purchasing.hillspire.com stays on hold (DNS at Network Solutions, ownership unclear).
Revisions on 25–26 October: 00011-cah (cutover, old look), 00014-nan (#225/#226,
had the 201 bug), 00016-kux (#228), 00018-poz (#229). 28 October: 00020-now (#242), 00022-goq (#248). 30 October: 00024-duj (#257). 7 November: 00026-san (#270, #260; checked: the tag
URL gave 308 on / and 403 on /api/session, the startup line read “mailbox on, Jira
writes on”, and signed out on the live URL / is 200, /app.js and /api/orders
are 401). Then 00028-qux (#272, Ask; the same checks, the startup line ending “Ask on
(claude-opus-5-5)”, and /api/ask 401 signed out). 8 November: 00030-kez (#273,
Reports; the same checks, and /api/report 403 on the tag URL, 401 signed out).
main only)Only merged code ships: build from a clean checkout of origin/main (a detached
worktree works), never from a feature branch or a dirty folder. Env values and secret
versions live on the service, so a routine deploy changes only the image; add
--update-env-vars only for a new setting. Promoting needs Adam’s explicit go.
P=hillspire-procurement; R=us-central1; S=purchasing-desk-pilot
cd integrations/procurement_workspace
npm ci && npm test
gcloud meta list-files-for-upload . # allowlisted sources only; every src/ file must appear
IMAGE=us-central1-docker.pkg.dev/$P/purchasing-desk/pilot:$(date +%Y%m%d)-main-$(git rev-parse --short HEAD)
gcloud builds submit . --project=$P --region=$R --config=cloudbuild.yaml \
--service-account=projects/$P/serviceAccounts/purchasing-desk-build@$P.iam.gserviceaccount.com \
--gcs-source-staging-dir=gs://hillspire-procurement-build-source/source --substitutions=_IMAGE=$IMAGE
DIGEST=$(gcloud artifacts docker images describe $IMAGE --project=$P --format='value(image_summary.fully_qualified_digest)')
# 1. Stage with no traffic.
gcloud run deploy $S --project=$P --region=$R --image=$DIGEST --no-traffic --tag=candidate
# 2. Check: the tag URL answers / with 308 and /api/session with 403, and the log shows
# "Purchasing Desk started: Okta sign-in, mailbox on, Jira writes on."
# 3. On Adam's go: promote BY NAME and drop the tag.
gcloud run services update-traffic $S --project=$P --region=$R \
--to-revisions=<new revision>=100 --remove-tags=candidate
# 4. Signed out, on the canonical URL: / is 200, /app.js and /api/orders are 401.
--update-env-vars=^;^NAME=a=1,b=2.desk.request_failed (path, upstream status),
desk.assign_failed, desk.comment_failed; successes are desk.applied.Check in Jira afterwards: title, Total Order Cost/Items, assignee, the public comments,
status, and that the history shows the Purchasing Desk account. Reporting:
project = PSA AND issue in updatedBy("712020:641e70b0-d5ba-4215-895a-bc029ea1622f")
lists every ticket the desk changed.
10824. Cost customfield_10883, items
customfield_10884, Entity customfield_10690.11118) → Awaiting approval (10944) → In Progress (3) →
“Ordered” (transition 31) → Shipped (11119) → “Confirm Delivery” → Closed (6).
The buyer clicks through the first two when picking a ticket up (seconds apart on
PSA-2605). The desk finds transitions by target status, not ID.712020:641e70b0-d5ba-4215-895a-bc029ea1622f (type
app). Jennifer 712020:1aee5f88-8c04-4d09-b621-54cc86f82436, Joe
712020:fd05c8b3-9bf1-4102-bae2-17eff3036436, Robbie 627a8b8a66eb5800697f0732,
Adam 6278b8c793111000689f3962.jsdPublic: true) with the heading
ORDER CONFIRMATION UPDATE: Vendor (order); the desk’s match them, and set
sd.public.comment {internal: false} explicitly.Accept-Language: en-US. Without it Node’s fetch sends *,
and Jira answered with Chinese status names (#223)., or
& between vendors, and the desk follows whichever the title already uses.Purchasing Desk, Authorization
Code only, redirect
https://purchasing-desk-pilot-343992350788.us-central1.run.app/auth/callback,
Login initiated by: App Only, client secret, Require PKCE. Client ID
0oa190iuv999gNscw698; group purchasingdesk-app-user.Purchasing Desk, JSM access,
PSA’s Service Desk Team role. Scoped token with only the classic scopes
read:jira-work and write:jira-work, one year at most, in
purchasing-desk-jira-desk-token.108320925923961802593, scope
https://www.googleapis.com/auth/gmail.readonly. Google cannot limit this to one
mailbox; the code does (subject fixed to procurement@, query fixed to Lenovo’s
sender).procurement@hillspire.com.Secrets go straight into Secret Manager, never into chat or files. On Windows from
Claude Code’s ! prompt (Git Bash, where /dev/clipboard comes back empty):
powershell.exe -NoProfile -Command "[Console]::Out.Write((Get-Clipboard -Raw).Trim())" | gcloud secrets versions add <name> --project=hillspire-procurement --data-file=-.
On a Mac, pbpaste | …. If gcloud says “Reauthentication failed”, run
gcloud auth login first; nothing is written when that error appears.
purchasing-desk-mail@hillspire-procurement.iam.gserviceaccount.com,
client ID 108320925923961802593, no keys; purchasing-desk-runtime@ holds
roles/iam.serviceAccountTokenCreator on it (that account only).purchasing_desk (US, 7 November): the order ledger, filled by log
sink purchasing-desk-ledger, whose writer is WRITER on this dataset only.purchasing-desk-runtime@ only:
purchasing-desk-session-key, purchasing-desk-okta-client-secret,
purchasing-desk-jira-desk-token (all version 1), and
purchasing-desk-anthropic-api-key (version 2; Ask). purchasing-desk-jira-api-token
is Adam’s old personal read token, to revoke.hosted-env.json.example lists them):
PROCUREMENT_OKTA_CLIENT_ID=0oa190iuv999gNscw698,
PROCUREMENT_JIRA_EMAIL=purchasing-desk-ws8a0vecb3@serviceaccount.atlassian.com,
PROCUREMENT_MAIL_SERVICE_ACCOUNT=purchasing-desk-mail@hillspire-procurement.iam.gserviceaccount.com,
PROCUREMENT_JIRA_ASSIGNEES as in the example file.Okta replaced IAP: traffic moved to the Okta build while IAP still guarded it
(00009-gef), Adam tested sign-in, Orders and Requests through IAP, then IAP came off
and allUsers became invoker (00011-cah). Signed-out checks passed: / 200,
/app.js and /api/orders 401, the second URL 308 to the first.
Rollback to the IAP pilot (only if the Okta desk must go dark): IAP back on
(gcloud run services update $S --iap), remove the allUsers invoker, route to
00007-les. That revision requires IAP’s signed header, so with IAP off it refuses
everyone rather than opening.
main, like HarryDevelopment continues here for a long time, so deploys move to a pipeline (Adam,
21 October), once a few real orders have proved the manual steps. Model it on
.github/workflows/harry_deploy.yml and harry_agent_tests.yml (Harry HANDOFF,
“Moving to merged to main = deployed”):
integrations/procurement_workspace/**, run npm test,
npm run build and the prettier check. Add npm run verify if the runner’s Chrome
works.cloudbuild.yaml and the
purchasing-desk-build@ account, stage a no-traffic tagged revision, check it,
promote it, confirm the live revision runs that commit, remove the tag.HARRY_AUTO_PROMOTE.integrations/deploy_log/).Differences from Harry to settle:
purchasing-desk-deployer@hillspire-procurement, through
Harry’s GitHub Workload Identity pool in gw-admin-assistant (cross-project) or a
new pool here. Scoped to this one service, plus actAs on the runtime and build
accounts only./ → 308 to the canonical
origin and /api/session → 403 (started, enforcing the host check, nothing more).
After promotion, on the canonical origin: / → 200, /api/session → 401. Confirm
the commit from the serving revision’s image tag through gcloud.--image only and never re-sends them.