It's not exactly clear what you mean by 'to get the first and last possible CDS GRanges'. That could be interpreted as wanting at most two GRanges per CDS, or what I assume you actually want, which would be to get a single GRanges item for each CDS that extends to the furthest extent of any underlying range.

I don't actually know how you would do the former. But the latter is simple:

reduce(unlist(range(cds_grl)))

which will give you a GRanges with just one range per gene, that extends to the furthest extent of all CDS from that gene.

Which is cool, but reduce will also take any overlapping, unrelated transcripts and then reduce them to a single range as well, which isn't cool. AND, this still won't give you a single GRanges item for a single gene if the CDS aren't overlapping, which is a thing.

I assumed by "gene" the OP meant "transcript". To do it by gene, you would need to use the gene IDs instead. Trans-splicing is another complication though, where there will be multiple ranges per transcript, on different sequences and/or strands. But it's easy to restrict to length one elements before doing the unlist().